Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
c988ef0
chore: remove black extension from devcontainer config
h4l Feb 1, 2025
723c8d8
build: add dev image target and compose service
h4l Feb 3, 2025
ed55251
feat: support Generic TypedDict in _pycompat.typing
h4l Feb 3, 2025
61ca0f2
feat: add explicit NotSet constant
h4l Feb 3, 2025
e5c7d8e
test: implement atomic_write on MockKvDb
h4l Oct 10, 2024
3a21ea0
feat: implement datapath.atomic_write()
h4l Oct 11, 2024
fc49e55
feat: add EvalEnumRepr Enum mixin
h4l Oct 12, 2024
f8af2b7
refactor: move standalone KV value types into submodule
h4l Feb 2, 2025
b68869c
feat: add types for Kv write API
h4l Feb 3, 2025
7d77861
feat: add create_default_v8_encoder()
h4l Oct 13, 2024
fcc8fd7
feat: implement Kv.write() API
h4l Feb 2, 2025
f7945b1
refactor: move mock Data Path API into denokv_testing
h4l Oct 13, 2024
cd9b0d7
refactor: use denokv_testing's mock HTTP API in test_kv
h4l Oct 13, 2024
2a01ecb
test: replace deprecated body= arg for HTTP exceptions
h4l Oct 13, 2024
f539d8f
refactor: move make_database_metadata_for_endpoint
h4l Oct 13, 2024
c0e429a
refactor: generalise make_database_metadata_for_endpoint
h4l Oct 13, 2024
7b99acd
refactor: remove mk_db_meta()
h4l Oct 13, 2024
659ad69
refactor: use mock db via datapath to test Kv.list()
h4l Feb 2, 2025
dd53441
test: use strict BigInt/Number V8 decoder in MockKvDb
h4l Oct 14, 2024
6ec537e
feat: support is_ok/is_err for Kv write() result type
h4l Oct 13, 2024
1c205a8
feat: include error details in ResponseUnsuccessful str
h4l Oct 14, 2024
c2d4a49
fix: don't use BaseException for DenoKvError
h4l Oct 19, 2024
a040e1f
feat: make ConsistencyLevel enum ordered
h4l Oct 26, 2024
ee0bd70
test: provide assertion errors for protobuf Message
h4l Feb 2, 2025
4069c9b
test: add mocked(...) helper to access Mock methods
h4l Dec 28, 2024
18896ce
chore: support __notes__ < py3.11
h4l Feb 2, 2025
66d6775
feat: support copying notes between exceptions
h4l Jan 16, 2025
4c3d945
chore: upgrade v8serialize to 0.2.0 (alpha)
h4l Dec 30, 2024
5831fb1
refactor!: remove customised v8serialize Encoder
h4l Jan 7, 2025
6b417f5
test: add typeval() util function
h4l Dec 31, 2024
db1fb35
feat: add @frozen class decorator
h4l Feb 2, 2025
d0a848f
fix: annotate KvU64.RANGE
h4l Jan 4, 2025
f9d2118
test: configure hypothesis profiles to run more examples
h4l Jan 5, 2025
9744433
feat: add Result.or_raise(), Result.value_or_raise()
h4l Jan 6, 2025
307cb4b
test: improve number type handling in mock KV
h4l Jan 9, 2025
e15f97a
test: allow AnyKvKey in denokv_testing.add_entries()
h4l Jan 16, 2025
1b510fd
test: only include denokv module in coverage reports
h4l Jan 15, 2025
1962427
feat: rework PlannedWrite & Mutation APIs
h4l Feb 3, 2025
8169df9
refactor: extract PlannedWrite methods as mixins
h4l Jan 17, 2025
c85705f
feat: allow DenoKvError to have no message argument
h4l Jan 29, 2025
1517bb0
feat: make FailedWrite, ConflictedWrite exceptions
h4l Feb 3, 2025
d13ba06
chore: enable mypy possibly-undefined check
h4l Jan 29, 2025
a8694de
chore: clarify ambiguously-defined variable
h4l Jan 29, 2025
b20372e
fix: allow CheckFailure with no failed indexes
h4l Jan 31, 2025
39ee062
fix: support unknown conflicts in write result types
h4l Feb 4, 2025
5a09e3f
feat: provide mutation shorthand methods on Kv
h4l Feb 1, 2025
e8431f6
refactor: adjust type annotations to support py39
h4l Feb 2, 2025
3fde257
refactor: use parse_rfc3339_datetime in tests
h4l Feb 3, 2025
45f1517
fix: prevent __dict__ creation on types with slots
h4l Feb 4, 2025
96c4e05
chore: enforce typing import ban with lint rule
h4l Feb 4, 2025
d07120c
style: auto-format markdown files
h4l Feb 4, 2025
9e18b91
docs: update README and CHANGELOG for write support
h4l Feb 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,7 @@
"postCreateCommand": "pipx install poetry",
"customizations": {
"vscode": {
"extensions": [
"charliermarsh.ruff",
"ms-python.python",
"ms-python.isort",
"ms-python.black-formatter"
]
"extensions": ["charliermarsh.ruff", "ms-python.python"]
}
}
}
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"tabWidth": 2,
"useTabs": false
"useTabs": false,
"proseWrap": "always"
}
14 changes: 12 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Added

- Support closing `Kv` client's `session` via: ([#20](https://github.com/h4l/denokv-python/pull/20))
- Support writing to KV databases with `Kv.set()`, `Kv.delete()`, `Kv.sum()`,
`Kv.min()`, `Kv.max()`, `Kv.enqueue()` and `Kv.check()`.
([#16](https://github.com/h4l/denokv-python/pull/16))

- These methods are available on `Kv` itself for one-off operations, and
`Kv.atomic()` can chain these methods to group write operations to apply
together in a transaction.

- Support closing `Kv` client's `session` via:
([#20](https://github.com/h4l/denokv-python/pull/20))
- `Kv.aclose()`
- async context manager
- At interpreter exit / garbage collection via `Kv.create_finalizer()`
- Automatically when an interactive console exists:
- `Kv` objects created by `open_kv()` from an interactive console/REPL automatically close at exit.
- `Kv` objects created by `open_kv()` from an interactive console/REPL
automatically close at exit.
- The `open_kv()` function has a `finalize` option that controls this.

[unreleased]: https://github.com/h4l/denokv-python/commits/main/
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,31 @@ _Connect to [Deno KV] cloud and [self-hosted] databases from Python._
[self-hosted]: https://deno.com/blog/kv-is-open-source-with-continuous-backup
[denokv server]: https://github.com/denoland/denokv

The `denokv` package is an unofficial Python client for the Deno KV database. It can connect to
both the distributed cloud KV service, or self-hosted [denokv server] (which can be a replica of a cloud KV database, or standalone).
The `denokv` package is an unofficial Python client for the Deno KV database. It
can connect to both the distributed cloud KV service, or self-hosted [denokv
server] (which can be a replica of a cloud KV database, or standalone).

It implements version 3 of the [KV Connect protocol spec, published by Deno](https://github.com/denoland/denokv/blob/main/proto/kv-connect.md).
It implements version 3 of the
[KV Connect protocol spec, published by Deno](https://github.com/denoland/denokv/blob/main/proto/kv-connect.md).

## Status

The package is under active development and is not yet stable or feature-complete.
The package is under active development and is not yet stable or
feature-complete.

**Working**:

- [x] Reading data with kv.get(), kv.list()
- [x] Reading data with `Kv.get()`, `Kv.list()`
- The read APIs are being reworked to improve ergonomics and functionality
- [x] Writing data with with `Kv.set()`, `Kv.delete()`, `Kv.sum()`, `Kv.min()`,
`Kv.max()`, `Kv.enqueue()` and `Kv.check()`.
- These methods are available on `Kv` itself for one-off operations, and
`Kv.atomic()` can chain these methods to group write operations to apply
together in a transaction.

**To-do**:

- [ ] [Writing data / transactions](https://docs.deno.com/deploy/kv/manual/transactions/)
- [ ] [Watching for changes](https://docs.deno.com/deploy/kv/manual/operations/#watch)
- [ ] [Queues](https://deno.com/blog/queues)
- This is uncertain: The KV Connect protocol does not support Queues, but they
could be implemented using watching in theory.
14 changes: 14 additions & 0 deletions docker-bake.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ function "get_py_image_tag" {

py_versions = ["3.9", "3.10", "3.11", "3.12", "3.13"]

target "dev" {
name = "dev_py${replace(py, ".", "")}"
matrix = {
py = py_versions,
}
args = {
PYTHON_VER = get_py_image_tag(py)
REPORT_CODE_COVERAGE = REPORT_CODE_COVERAGE
REPORT_CODE_BRANCH_COVERAGE = REPORT_CODE_BRANCH_COVERAGE
}
target = "poetry"
tags = ["ghcr.io/h4l/denokv-python/dev:py${replace(py, ".", "")}"]
}

target "test" {
name = "test_py${replace(py, ".", "")}"
matrix = {
Expand Down
13 changes: 13 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ services:
networks:
- devcontainer_denokv_python

dev-py39:
profiles: [dev-py39]
image: ghcr.io/h4l/denokv-python/dev:py39
volumes:
- workspace:/workspaces
working_dir: /workspaces/denokv-python
environment:
PYTHONPATH: /workspaces/denokv-python/src
command: poetry run ipython
networks:
- devcontainer_denokv_python


networks:
devcontainer_denokv_python:
external: true
Expand Down
8 changes: 4 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ protobuf = ">=4.22.0,<6"
# change was in 6.2.4 (which is late 2019)
# https://github.com/apple/foundationdb/commits/main/bindings/python/fdb/tuple.py
foundationdb = ">=6.2.4,<8"
v8serialize = "^0.1.0"
v8serialize = "^0.2.0-alpha.0"

[tool.poetry.group.dev.dependencies]
pytest = "^8.3.2"
Expand Down Expand Up @@ -57,6 +57,9 @@ extra_standard_library = ["typing_extensions"]

[tool.mypy]
strict = true
enable_error_code = [
'possibly-undefined'
]
mypy_path = "./stubs"

[[tool.mypy.overrides]]
Expand Down Expand Up @@ -84,6 +87,7 @@ select = [
"FA", # flake8-future-annotations
"PYI", # flake8-pyi
"I",
"TID", # flake8-tidy-imports
]

ignore = [
Expand All @@ -99,6 +103,10 @@ ignore = [
"PYI041",
]

[tool.ruff.lint.flake8-tidy-imports.banned-api]
"typing_extensions".msg = "use denokv._pycompat.typing instead, typing_extensions is not a runtime dependency."
"typing".msg = "Use denokv._pycompat.typing instead (apart from overload and Literal), using typing is error-prone as it has many differences between python versions."

[tool.ruff.lint.pydocstyle]
convention = "numpy"

Expand All @@ -114,6 +122,7 @@ filterwarnings = [
]

[tool.coverage.run]
source = ["denokv"]
omit = [
# generated Protocol Buffers module
"*/denokv/_datapath_pb2.py",
Expand Down
6 changes: 3 additions & 3 deletions src/denokv/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from __future__ import annotations

from denokv._kv_values import KvEntry as KvEntry
from denokv._kv_values import KvU64 as KvU64
from denokv._kv_values import VersionStamp as VersionStamp
from denokv.auth import ConsistencyLevel as ConsistencyLevel
from denokv.auth import MetadataExchangeDenoKvError as MetadataExchangeDenoKvError
from denokv.datapath import AnyKvKey as AnyKvKey
Expand All @@ -13,10 +16,7 @@
from denokv.kv import CursorFormatType as CursorFormatType
from denokv.kv import Kv as Kv
from denokv.kv import KvCredentials as KvCredentials
from denokv.kv import KvEntry as KvEntry
from denokv.kv import KvListOptions as KvListOptions
from denokv.kv import KvU64 as KvU64
from denokv.kv import ListKvEntry as ListKvEntry
from denokv.kv import VersionStamp as VersionStamp
from denokv.kv import open_kv as open_kv
from denokv.kv_keys import KvKey as KvKey
87 changes: 87 additions & 0 deletions src/denokv/_kv_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from __future__ import annotations

from abc import ABC
from abc import abstractmethod

from google.protobuf.message import Message
from v8serialize import Encoder

from denokv._datapath_pb2 import AtomicWrite
from denokv._kv_values import VersionStamp
from denokv._pycompat.typing import Generic
from denokv._pycompat.typing import Protocol
from denokv._pycompat.typing import Sequence
from denokv._pycompat.typing import TypeAlias
from denokv._pycompat.typing import TypeGuard
from denokv._pycompat.typing import TypeVar
from denokv._pycompat.typing import Union
from denokv.auth import EndpointInfo
from denokv.datapath import CheckFailure
from denokv.datapath import DataPathError
from denokv.result import Nothing
from denokv.result import Option
from denokv.result import Result
from denokv.result import Some

WriteResultT = TypeVar("WriteResultT")
WriteResultT_co = TypeVar("WriteResultT_co", covariant=True)
MessageT_co = TypeVar("MessageT_co", bound=Message, covariant=True)


class ProtobufMessageRepresentation(Generic[MessageT_co], ABC):
"""An object that can represent itself as a protobuf Messages."""

__slots__ = ()

@abstractmethod
def as_protobuf(self, *, v8_encoder: Encoder) -> Sequence[MessageT_co]: ...


class SingleProtobufMessageRepresentation(ProtobufMessageRepresentation[MessageT_co]):
"""An object that can represent itself as a single protobuf Message."""

__slots__ = ()

@abstractmethod
def as_protobuf(self, *, v8_encoder: Encoder) -> tuple[MessageT_co]: ...


class AtomicWriteRepresentation(SingleProtobufMessageRepresentation[AtomicWrite]):
__slots__ = ()


class AtomicWriteRepresentationWriter(
AtomicWriteRepresentation, Generic[WriteResultT_co]
):
__slots__ = ()

@abstractmethod
async def write(self, kv: KvWriter, *, v8_encoder: Encoder) -> WriteResultT_co: ...


KvWriterWriteResult: TypeAlias = Result[
tuple[VersionStamp, EndpointInfo], Union[CheckFailure, DataPathError]
]


class KvWriter(ABC):
"""A low-level interface for objects that can perform KV writes."""

@abstractmethod
async def write(self, *, protobuf_atomic_write: AtomicWrite) -> KvWriterWriteResult:
"""Write a protobuf AtomicWrite message to the database."""


class V8EncoderProvider(Protocol):
@property
def v8_encoder(self) -> Encoder: ...


def is_v8_encoder_provider(obj: object) -> TypeGuard[V8EncoderProvider]:
return isinstance(getattr(obj, "v8_encoder", None), Encoder)


def get_v8_encoder(maybe_v8_encoder_provider: object) -> Option[Encoder]:
if is_v8_encoder_provider(maybe_v8_encoder_provider):
return Some(maybe_v8_encoder_provider.v8_encoder)
return Nothing()
Loading
Loading