ktables 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ pypi:
9
+ runs-on: ubuntu-latest
10
+ environment: pypi
11
+ permissions:
12
+ id-token: write # OIDC for PyPI trusted publishing — no API token needed
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v5
16
+ - name: Build sdist and wheel
17
+ run: uv build
18
+ - name: Publish
19
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,17 @@
1
+ name: Release Please
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ permissions:
8
+ contents: write
9
+ pull-requests: write
10
+
11
+ jobs:
12
+ release-please:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: googleapis/release-please-action@v4
16
+ with:
17
+ release-type: python
@@ -0,0 +1,40 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ unit:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v5
18
+ - name: Run unit tests (integration auto-skips without a broker)
19
+ run: uv run --python ${{ matrix.python-version }} pytest -rs
20
+
21
+ integration:
22
+ runs-on: ubuntu-latest
23
+ services:
24
+ kafka:
25
+ image: apache/kafka:3.9.0
26
+ ports:
27
+ - 9092:9092
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: astral-sh/setup-uv@v5
31
+ - name: Wait for Kafka
32
+ run: |
33
+ for i in $(seq 1 30); do
34
+ (echo > /dev/tcp/localhost/9092) 2>/dev/null && exit 0
35
+ sleep 2
36
+ done
37
+ echo "Kafka did not become reachable" >&2
38
+ exit 1
39
+ - name: Run full suite against the broker
40
+ run: uv run --python 3.12 pytest -rs
@@ -0,0 +1,218 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
205
+
206
+ # Ruff stuff:
207
+ .ruff_cache/
208
+
209
+ # PyPI configuration file
210
+ .pypirc
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
216
+
217
+ # Streamlit
218
+ .streamlit/secrets.toml
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-06-10)
4
+
5
+
6
+ ### Features
7
+
8
+ * KafkaTable and KafkaTableWriter — a GlobalKTable for asyncio Python ([#1](https://github.com/ryan-yuuu/ktables/issues/1)) ([4a2b0b9](https://github.com/ryan-yuuu/ktables/commit/4a2b0b9ecfea398d6c8bdcfcfba23f828c1566b0))
ktables-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ryan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ktables-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,200 @@
1
+ Metadata-Version: 2.4
2
+ Name: ktables
3
+ Version: 0.1.0
4
+ Summary: Materialize a Kafka topic into an in-memory, compacted dict — a GlobalKTable for asyncio Python.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: aiokafka>=0.10
9
+ Description-Content-Type: text/markdown
10
+
11
+ # ktables
12
+
13
+ [![Tests](https://github.com/ryan-yuuu/ktables/actions/workflows/test.yml/badge.svg)](https://github.com/ryan-yuuu/ktables/actions/workflows/test.yml)
14
+ ![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)
15
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
16
+
17
+ Materialize a Kafka topic into an in-memory, compacted dict — a GlobalKTable for asyncio Python.
18
+
19
+ Every process that opens a `KafkaTable` replays the topic from the beginning
20
+ into a local read-only mapping, then keeps consuming for live updates; a
21
+ `KafkaTableWriter` maintains the topic with keyed upserts and tombstones.
22
+ Built for small, broadly-needed reference data — service registries,
23
+ capability advertisements, feature flags, config maps — not for large or
24
+ high-churn state.
25
+
26
+ ## Table of Contents
27
+
28
+ - [Background](#background)
29
+ - [Install](#install)
30
+ - [Usage](#usage)
31
+ - [Consistency contract](#consistency-contract)
32
+ - [API](#api)
33
+ - [Contributing](#contributing)
34
+ - [License](#license)
35
+
36
+ ## Background
37
+
38
+ Kafka Streams (JVM) has two table abstractions over changelog topics: the
39
+ partition-sharded `KTable`, where each application instance holds a slice of
40
+ the keys, and `GlobalKTable`, where every instance bootstraps and maintains a
41
+ full local copy — the right shape for lookup data that any instance may need
42
+ at any moment. The Python ecosystem has several maintained stream-processing
43
+ frameworks, but all of them implement only the sharded shape, with
44
+ framework-owned changelog topics and their own process runtimes. ktables fills
45
+ the gap with just the global-table piece, as a plain asyncio library over
46
+ `aiokafka`: your topic, your message format, your event loop.
47
+
48
+ The Kafka semantics the implementation relies on (group-less consumers,
49
+ catch-up gating against end offsets, compaction independence) are documented,
50
+ with provenance, in the module docstring of
51
+ [`kafka_table.py`](./ktables/kafka_table.py).
52
+
53
+ ## Install
54
+
55
+ ktables is not yet published to PyPI. Until the first release, vendor the
56
+ package — copy the `ktables/` directory into your project — and install its
57
+ single runtime dependency:
58
+
59
+ ```sh
60
+ $ pip install aiokafka
61
+ ```
62
+
63
+ Requires Python 3.10+. Pydantic is **not** required — the `.json()` presets
64
+ accept any class with pydantic-v2's JSON methods.
65
+
66
+ ## Usage
67
+
68
+ Maintain a registry from one service:
69
+
70
+ ```python
71
+ from ktables import KafkaTableWriter
72
+
73
+ writer = KafkaTableWriter.json(
74
+ bootstrap_servers="localhost:9092", topic="my.registry", model=ServiceRecord
75
+ )
76
+ async with writer:
77
+ await writer.set("billing", record) # upsert (broker-acked)
78
+ ...
79
+ await writer.delete("billing") # tombstone: removes the key
80
+ ```
81
+
82
+ Consume it from any other process:
83
+
84
+ ```python
85
+ from ktables import KafkaTable
86
+
87
+ table = KafkaTable.json(
88
+ bootstrap_servers="localhost:9092", topic="my.registry", model=ServiceRecord
89
+ )
90
+ async with table: # replays the topic; returns once caught up
91
+ record = table.get("billing")
92
+ if table.status != "caught_up": # "degraded": catch-up timed out
93
+ ...
94
+ ```
95
+
96
+ Non-pydantic payloads: construct directly with your own codecs —
97
+ `KafkaTable(..., value_decoder=bytes_to_value)` /
98
+ `KafkaTableWriter(..., value_encoder=value_to_bytes)`.
99
+
100
+ ### Removing a key on clean shutdown
101
+
102
+ There is deliberately no `delete_on_close` option (shutdown-time deletion is
103
+ application policy, and no library can promise it on a crash). Compose it:
104
+
105
+ ```python
106
+ async with writer:
107
+ await writer.set(my_key, my_record)
108
+ try:
109
+ ... # serve
110
+ finally:
111
+ await writer.delete(my_key) # acked before the producer stops
112
+ ```
113
+
114
+ ### Locked-down clusters
115
+
116
+ Both classes ensure their topic exists at start (idempotent create,
117
+ compacted). If the application lacks topic-create ACLs, pass
118
+ `ensure_topic=False` and create the topic out-of-band (the module-level
119
+ `ensure_topic()` function is the deploy-time primitive).
120
+
121
+ ## Consistency contract
122
+
123
+ `KafkaTable` is eventually consistent. Precisely:
124
+
125
+ 1. When `start()` / `async with` returns, contents are complete as of the
126
+ topic's end offsets at start time — unless `status == "degraded"`
127
+ (catch-up timed out; data may be partial).
128
+ 2. Thereafter, updates appear within milliseconds of the broker write — but
129
+ **there is no read-your-own-writes**: after `await writer.set(k, v)`, a
130
+ table in the same process may briefly still return the old value.
131
+ 3. Contents are stable between your awaits (single event loop; only the
132
+ reader task mutates). Use `snapshot()` for a copy held across awaits.
133
+ 4. Correctness does not depend on broker-side compaction: last-write-wins
134
+ over the full log yields the same dict; compaction only bounds replay time.
135
+
136
+ A tombstone is a record with a **null** value (`b""` is data, not a tombstone).
137
+ If the background reader dies (non-retriable error, e.g. authorization),
138
+ contents freeze at the last applied state: `status` becomes `"failed"` and
139
+ `failure` holds the exception — gate liveness decisions on `status`, never on
140
+ reads alone. Transient broker outages do not kill the reader; it resumes.
141
+
142
+ ## API
143
+
144
+ ### `KafkaTable[V]` — read-only `Mapping[str, V]`
145
+
146
+ | Member | Description |
147
+ |---|---|
148
+ | `KafkaTable(*, bootstrap_servers, topic, value_decoder, key_decoder=utf-8, catchup_timeout=30.0, poll_timeout_ms=200, ensure_topic=True, topic_configs=None)` | Construct (does not connect). |
149
+ | `KafkaTable.json(*, bootstrap_servers, topic, model, **kwargs)` | Preset wiring `model.model_validate_json` as the decoder. |
150
+ | `start()` / `stop()` / `async with` | Lifecycle. `start()` raises on double-start, missing topic, or reader death during catch-up; on catch-up *timeout* it serves degraded. |
151
+ | `table[key]`, `key in table`, `iter`, `len`, `.get(key, default=None)` | Mapping reads. Raise `RuntimeError` before `start()`. |
152
+ | `snapshot()` | Shallow-copy dict, safe to hold across awaits. |
153
+ | `status` | `"unstarted" \| "loading" \| "caught_up" \| "degraded" \| "failed"`. |
154
+ | `failure` | Exception that killed the reader, else `None`. |
155
+ | `is_caught_up` / `wait_until_caught_up(timeout=None)` | Catch-up gate; the wait returns `False` on timeout or reader death. |
156
+ | `stats` | Frozen `ViewStats` snapshot (see below). |
157
+
158
+ Equality is **identity** and instances are hashable: a running table is a
159
+ resource handle, not a value.
160
+
161
+ ### `KafkaTableWriter[V]`
162
+
163
+ | Member | Description |
164
+ |---|---|
165
+ | `KafkaTableWriter(*, bootstrap_servers, topic, value_encoder, key_encoder=utf-8, ensure_topic=True, topic_configs=None, enable_idempotence=True)` | Construct. Idempotence implies `acks=all` (registry-grade durability); opt out for throwaway data. |
166
+ | `KafkaTableWriter.json(*, bootstrap_servers, topic, model=None, **kwargs)` | Preset encoding via `model_dump_json()` (`model` is typing-only). |
167
+ | `set(key, value)` | Keyed upsert; awaits broker ack. Re-`set` periodically as a heartbeat. |
168
+ | `delete(key)` | Publishes a null-value tombstone; awaits broker ack. |
169
+ | `start()` / `stop()` / `async with` | Lifecycle; `set`/`delete` before start raise `RuntimeError`. |
170
+
171
+ The key encoder must be deterministic and stable across processes — on a
172
+ multi-partition topic, per-key ordering holds only if a key always hashes to
173
+ the same partition.
174
+
175
+ ### Module level
176
+
177
+ | Member | Description |
178
+ |---|---|
179
+ | `ensure_topic(bootstrap_servers, topic, *, num_partitions=1, replication_factor=1, topic_configs=None) -> bool` | Idempotent explicit create; `True` if this call created it. Defaults are dev-grade — production registries want RF≥3, `min.insync.replicas=2`, `acks=all`. |
180
+ | `DEFAULT_TOPIC_CONFIGS` | `{"cleanup.policy": "compact"}` (read-only mapping). |
181
+ | `ViewStats` | Frozen counters: `records_applied`, `tombstones_applied`, `keyless_records`, `key_decode_errors`, `value_decode_errors`, `catch_up_seconds`, `replayed_at_catch_up`. |
182
+ | `SupportsJsonModel` | Protocol the `.json()` presets require (`model_dump_json` / `model_validate_json`). |
183
+ | `TableStatus` | The `status` literal type. |
184
+
185
+ ## Contributing
186
+
187
+ Questions and bug reports are welcome as issues, and PRs are accepted. Please
188
+ run the test suite before submitting:
189
+
190
+ ```sh
191
+ $ pytest tests
192
+ ```
193
+
194
+ Unit tests always run; integration tests need a Kafka broker on
195
+ `localhost:9092` and skip otherwise
196
+ (`docker run -d -p 9092:9092 apache/kafka:3.9.0`).
197
+
198
+ ## License
199
+
200
+ [MIT](LICENSE)