alt-python-pynosqlc-cassandra 1.0.4__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.
- alt_python_pynosqlc_cassandra-1.0.4/.gitignore +30 -0
- alt_python_pynosqlc_cassandra-1.0.4/PKG-INFO +177 -0
- alt_python_pynosqlc_cassandra-1.0.4/README.md +152 -0
- alt_python_pynosqlc_cassandra-1.0.4/pynosqlc/cassandra/__init__.py +17 -0
- alt_python_pynosqlc_cassandra-1.0.4/pynosqlc/cassandra/cassandra_client.py +49 -0
- alt_python_pynosqlc_cassandra-1.0.4/pynosqlc/cassandra/cassandra_collection.py +113 -0
- alt_python_pynosqlc_cassandra-1.0.4/pynosqlc/cassandra/cassandra_driver.py +94 -0
- alt_python_pynosqlc_cassandra-1.0.4/pyproject.toml +49 -0
- alt_python_pynosqlc_cassandra-1.0.4/tests/__init__.py +0 -0
- alt_python_pynosqlc_cassandra-1.0.4/tests/test_compliance.py +66 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
|
|
2
|
+
# ── GSD baseline (auto-generated) ──
|
|
3
|
+
.gsd
|
|
4
|
+
.DS_Store
|
|
5
|
+
Thumbs.db
|
|
6
|
+
*.swp
|
|
7
|
+
*.swo
|
|
8
|
+
*~
|
|
9
|
+
.idea/
|
|
10
|
+
.vscode/
|
|
11
|
+
*.code-workspace
|
|
12
|
+
.env
|
|
13
|
+
.env.*
|
|
14
|
+
!.env.example
|
|
15
|
+
node_modules/
|
|
16
|
+
.next/
|
|
17
|
+
dist/
|
|
18
|
+
build/
|
|
19
|
+
__pycache__/
|
|
20
|
+
*.pyc
|
|
21
|
+
.venv/
|
|
22
|
+
venv/
|
|
23
|
+
target/
|
|
24
|
+
vendor/
|
|
25
|
+
*.log
|
|
26
|
+
coverage/
|
|
27
|
+
.cache/
|
|
28
|
+
tmp/
|
|
29
|
+
|
|
30
|
+
/.bg-shell/
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: alt-python-pynosqlc-cassandra
|
|
3
|
+
Version: 1.0.4
|
|
4
|
+
Summary: Cassandra driver for pynosqlc
|
|
5
|
+
Project-URL: Homepage, https://github.com/alt-python/pynosqlc
|
|
6
|
+
Project-URL: Repository, https://github.com/alt-python/pynosqlc
|
|
7
|
+
Project-URL: Documentation, https://github.com/alt-python/pynosqlc#getting-started
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/alt-python/pynosqlc/issues
|
|
9
|
+
Author: Craig Parravicini, Claude (Anthropic)
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: async,cassandra,database,driver,nosql
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Database
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: alt-python-pynosqlc-core
|
|
22
|
+
Requires-Dist: alt-python-pynosqlc-memory
|
|
23
|
+
Requires-Dist: cassandra-driver>=3.29.1
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# pynosqlc-cassandra
|
|
27
|
+
|
|
28
|
+
Cassandra 4 driver for [pynosqlc](https://github.com/alt-python/pynosqlc) — a
|
|
29
|
+
JDBC-inspired unified async NoSQL access layer for Python.
|
|
30
|
+
|
|
31
|
+
Install this driver to connect pynosqlc to a Cassandra 4 instance using the
|
|
32
|
+
cassandra-driver library, bridged into Python's asyncio via
|
|
33
|
+
`asyncio.run_in_executor`. All pynosqlc operations — `store`, `get`, `insert`,
|
|
34
|
+
`update`, `delete`, and `find` — are supported.
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- Python 3.12+
|
|
39
|
+
- Cassandra 4.0+ instance (local or remote)
|
|
40
|
+
- `pynosqlc-core` and `pynosqlc-memory` (installed automatically as dependencies)
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install alt-python-pynosqlc-cassandra
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import asyncio
|
|
52
|
+
from pynosqlc.core import DriverManager, Filter
|
|
53
|
+
import pynosqlc.cassandra # auto-registers CassandraDriver on import
|
|
54
|
+
|
|
55
|
+
async def main():
|
|
56
|
+
async with await DriverManager.get_client(
|
|
57
|
+
'pynosqlc:cassandra:localhost:9042/my_keyspace'
|
|
58
|
+
) as client:
|
|
59
|
+
col = client.get_collection('orders')
|
|
60
|
+
|
|
61
|
+
# Store a document at a known key (upsert semantics)
|
|
62
|
+
await col.store('order-001', {'item': 'widget', 'qty': 5, 'status': 'pending'})
|
|
63
|
+
|
|
64
|
+
# Retrieve a document by key
|
|
65
|
+
doc = await col.get('order-001')
|
|
66
|
+
print(doc) # {'item': 'widget', 'qty': 5, 'status': 'pending', '_id': 'order-001'}
|
|
67
|
+
|
|
68
|
+
# Insert a document with a driver-assigned key
|
|
69
|
+
key = await col.insert({'item': 'gadget', 'qty': 2, 'status': 'pending'})
|
|
70
|
+
print(key) # e.g. 'a1b2c3d4-...'
|
|
71
|
+
|
|
72
|
+
# Update fields (shallow merge — only listed fields change)
|
|
73
|
+
await col.update('order-001', {'qty': 10, 'status': 'shipped'})
|
|
74
|
+
|
|
75
|
+
# Find documents matching a filter
|
|
76
|
+
f = Filter.where('status').eq('pending').build()
|
|
77
|
+
async for doc in await col.find(f):
|
|
78
|
+
print(doc)
|
|
79
|
+
|
|
80
|
+
# Delete a document
|
|
81
|
+
await col.delete('order-001')
|
|
82
|
+
|
|
83
|
+
asyncio.run(main())
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## URL Scheme
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
pynosqlc:cassandra:<host>:<port>/<keyspace>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| URL | Description |
|
|
93
|
+
|-----|-------------|
|
|
94
|
+
| `pynosqlc:cassandra:localhost:9042/my_keyspace` | Local Cassandra, named keyspace |
|
|
95
|
+
| `pynosqlc:cassandra:cassandra.example.com:9042/prod` | Remote instance |
|
|
96
|
+
|
|
97
|
+
The `<keyspace>` segment is required. If the keyspace does not exist, the driver
|
|
98
|
+
creates it automatically using `SimpleStrategy` with replication factor 1. For
|
|
99
|
+
production use, create the keyspace manually with appropriate replication
|
|
100
|
+
settings before connecting.
|
|
101
|
+
|
|
102
|
+
## Schema
|
|
103
|
+
|
|
104
|
+
Each pynosqlc collection maps to a Cassandra table in the configured keyspace:
|
|
105
|
+
|
|
106
|
+
```cql
|
|
107
|
+
CREATE TABLE IF NOT EXISTS <collection_name> (
|
|
108
|
+
pk TEXT PRIMARY KEY,
|
|
109
|
+
data TEXT
|
|
110
|
+
);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- `pk` — the document key (string)
|
|
114
|
+
- `data` — the document serialised as a JSON string
|
|
115
|
+
|
|
116
|
+
Tables are created automatically on the first operation against a collection.
|
|
117
|
+
You do not need to create tables manually.
|
|
118
|
+
|
|
119
|
+
## Filtering
|
|
120
|
+
|
|
121
|
+
Filters are evaluated **in-process** after a full table scan. `find()` fetches
|
|
122
|
+
every row from the collection table, deserialises the `data` column, then
|
|
123
|
+
applies the pynosqlc filter AST in memory using `MemoryFilterEvaluator`. There
|
|
124
|
+
is no CQL WHERE clause generated.
|
|
125
|
+
|
|
126
|
+
This matches the design of the jsnosqlc Cassandra driver and is appropriate for
|
|
127
|
+
development, testing, and moderate-sized collections. For production workloads
|
|
128
|
+
with large tables, evaluate CQL secondary indexes or materialised views as
|
|
129
|
+
complementary tools.
|
|
130
|
+
|
|
131
|
+
All filter operators are supported: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`,
|
|
132
|
+
`contains`, `in_`, `nin`, `exists`, and their `and_` / `or_` / `not_`
|
|
133
|
+
combinators.
|
|
134
|
+
|
|
135
|
+
## Async Integration
|
|
136
|
+
|
|
137
|
+
cassandra-driver uses a synchronous API. This driver wraps every blocking CQL
|
|
138
|
+
call in `asyncio.get_event_loop().run_in_executor(None, ...)` so the asyncio
|
|
139
|
+
event loop is never blocked. The default cassandra-driver reactor
|
|
140
|
+
(thread-based) is used. Do not set `connection_class=AsyncioConnection` —
|
|
141
|
+
`AsyncioConnection` hooks into the event loop from the main thread and cannot
|
|
142
|
+
be used from a thread-pool executor.
|
|
143
|
+
|
|
144
|
+
## Troubleshooting
|
|
145
|
+
|
|
146
|
+
**`NoHostAvailable` or `ConnectionException` on connect**
|
|
147
|
+
Cassandra is not running or is not reachable on the configured host and port.
|
|
148
|
+
Verify with `cqlsh <host> <port>` — you should reach the CQL shell prompt.
|
|
149
|
+
|
|
150
|
+
**`ImportError: No module named 'pynosqlc.cassandra'`**
|
|
151
|
+
The package is not installed. Run `pip install alt-python-pynosqlc-cassandra`.
|
|
152
|
+
|
|
153
|
+
**`ValueError: No driver found for URL ...`**
|
|
154
|
+
The import `import pynosqlc.cassandra` was not executed before calling
|
|
155
|
+
`DriverManager.get_client(...)`. The import is what triggers driver
|
|
156
|
+
registration — it must come before any `get_client` call.
|
|
157
|
+
|
|
158
|
+
**`InvalidRequest: Keyspace '<name>' does not exist`**
|
|
159
|
+
This should not occur in normal use because the driver creates the keyspace
|
|
160
|
+
automatically. If you see this error, check that the connecting user has
|
|
161
|
+
`CREATE KEYSPACE` permission, or create the keyspace manually:
|
|
162
|
+
|
|
163
|
+
```cql
|
|
164
|
+
CREATE KEYSPACE my_keyspace
|
|
165
|
+
WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Filter returns no results despite documents being present**
|
|
169
|
+
Confirm you called `.build()` at the end of your filter chain:
|
|
170
|
+
`Filter.where('field').eq('value').build()`. Passing an unbuilt `FieldCondition`
|
|
171
|
+
object instead of the built `dict` will match nothing.
|
|
172
|
+
|
|
173
|
+
## Further Reading
|
|
174
|
+
|
|
175
|
+
- [pynosqlc API reference](../../docs/api-reference.md) — complete method signatures
|
|
176
|
+
- [Driver implementation guide](../../docs/driver-guide.md) — how pynosqlc drivers work
|
|
177
|
+
- [Getting started tutorial](../../docs/getting-started.md) — step-by-step introduction
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# pynosqlc-cassandra
|
|
2
|
+
|
|
3
|
+
Cassandra 4 driver for [pynosqlc](https://github.com/alt-python/pynosqlc) — a
|
|
4
|
+
JDBC-inspired unified async NoSQL access layer for Python.
|
|
5
|
+
|
|
6
|
+
Install this driver to connect pynosqlc to a Cassandra 4 instance using the
|
|
7
|
+
cassandra-driver library, bridged into Python's asyncio via
|
|
8
|
+
`asyncio.run_in_executor`. All pynosqlc operations — `store`, `get`, `insert`,
|
|
9
|
+
`update`, `delete`, and `find` — are supported.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Python 3.12+
|
|
14
|
+
- Cassandra 4.0+ instance (local or remote)
|
|
15
|
+
- `pynosqlc-core` and `pynosqlc-memory` (installed automatically as dependencies)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install alt-python-pynosqlc-cassandra
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import asyncio
|
|
27
|
+
from pynosqlc.core import DriverManager, Filter
|
|
28
|
+
import pynosqlc.cassandra # auto-registers CassandraDriver on import
|
|
29
|
+
|
|
30
|
+
async def main():
|
|
31
|
+
async with await DriverManager.get_client(
|
|
32
|
+
'pynosqlc:cassandra:localhost:9042/my_keyspace'
|
|
33
|
+
) as client:
|
|
34
|
+
col = client.get_collection('orders')
|
|
35
|
+
|
|
36
|
+
# Store a document at a known key (upsert semantics)
|
|
37
|
+
await col.store('order-001', {'item': 'widget', 'qty': 5, 'status': 'pending'})
|
|
38
|
+
|
|
39
|
+
# Retrieve a document by key
|
|
40
|
+
doc = await col.get('order-001')
|
|
41
|
+
print(doc) # {'item': 'widget', 'qty': 5, 'status': 'pending', '_id': 'order-001'}
|
|
42
|
+
|
|
43
|
+
# Insert a document with a driver-assigned key
|
|
44
|
+
key = await col.insert({'item': 'gadget', 'qty': 2, 'status': 'pending'})
|
|
45
|
+
print(key) # e.g. 'a1b2c3d4-...'
|
|
46
|
+
|
|
47
|
+
# Update fields (shallow merge — only listed fields change)
|
|
48
|
+
await col.update('order-001', {'qty': 10, 'status': 'shipped'})
|
|
49
|
+
|
|
50
|
+
# Find documents matching a filter
|
|
51
|
+
f = Filter.where('status').eq('pending').build()
|
|
52
|
+
async for doc in await col.find(f):
|
|
53
|
+
print(doc)
|
|
54
|
+
|
|
55
|
+
# Delete a document
|
|
56
|
+
await col.delete('order-001')
|
|
57
|
+
|
|
58
|
+
asyncio.run(main())
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## URL Scheme
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
pynosqlc:cassandra:<host>:<port>/<keyspace>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
| URL | Description |
|
|
68
|
+
|-----|-------------|
|
|
69
|
+
| `pynosqlc:cassandra:localhost:9042/my_keyspace` | Local Cassandra, named keyspace |
|
|
70
|
+
| `pynosqlc:cassandra:cassandra.example.com:9042/prod` | Remote instance |
|
|
71
|
+
|
|
72
|
+
The `<keyspace>` segment is required. If the keyspace does not exist, the driver
|
|
73
|
+
creates it automatically using `SimpleStrategy` with replication factor 1. For
|
|
74
|
+
production use, create the keyspace manually with appropriate replication
|
|
75
|
+
settings before connecting.
|
|
76
|
+
|
|
77
|
+
## Schema
|
|
78
|
+
|
|
79
|
+
Each pynosqlc collection maps to a Cassandra table in the configured keyspace:
|
|
80
|
+
|
|
81
|
+
```cql
|
|
82
|
+
CREATE TABLE IF NOT EXISTS <collection_name> (
|
|
83
|
+
pk TEXT PRIMARY KEY,
|
|
84
|
+
data TEXT
|
|
85
|
+
);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- `pk` — the document key (string)
|
|
89
|
+
- `data` — the document serialised as a JSON string
|
|
90
|
+
|
|
91
|
+
Tables are created automatically on the first operation against a collection.
|
|
92
|
+
You do not need to create tables manually.
|
|
93
|
+
|
|
94
|
+
## Filtering
|
|
95
|
+
|
|
96
|
+
Filters are evaluated **in-process** after a full table scan. `find()` fetches
|
|
97
|
+
every row from the collection table, deserialises the `data` column, then
|
|
98
|
+
applies the pynosqlc filter AST in memory using `MemoryFilterEvaluator`. There
|
|
99
|
+
is no CQL WHERE clause generated.
|
|
100
|
+
|
|
101
|
+
This matches the design of the jsnosqlc Cassandra driver and is appropriate for
|
|
102
|
+
development, testing, and moderate-sized collections. For production workloads
|
|
103
|
+
with large tables, evaluate CQL secondary indexes or materialised views as
|
|
104
|
+
complementary tools.
|
|
105
|
+
|
|
106
|
+
All filter operators are supported: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`,
|
|
107
|
+
`contains`, `in_`, `nin`, `exists`, and their `and_` / `or_` / `not_`
|
|
108
|
+
combinators.
|
|
109
|
+
|
|
110
|
+
## Async Integration
|
|
111
|
+
|
|
112
|
+
cassandra-driver uses a synchronous API. This driver wraps every blocking CQL
|
|
113
|
+
call in `asyncio.get_event_loop().run_in_executor(None, ...)` so the asyncio
|
|
114
|
+
event loop is never blocked. The default cassandra-driver reactor
|
|
115
|
+
(thread-based) is used. Do not set `connection_class=AsyncioConnection` —
|
|
116
|
+
`AsyncioConnection` hooks into the event loop from the main thread and cannot
|
|
117
|
+
be used from a thread-pool executor.
|
|
118
|
+
|
|
119
|
+
## Troubleshooting
|
|
120
|
+
|
|
121
|
+
**`NoHostAvailable` or `ConnectionException` on connect**
|
|
122
|
+
Cassandra is not running or is not reachable on the configured host and port.
|
|
123
|
+
Verify with `cqlsh <host> <port>` — you should reach the CQL shell prompt.
|
|
124
|
+
|
|
125
|
+
**`ImportError: No module named 'pynosqlc.cassandra'`**
|
|
126
|
+
The package is not installed. Run `pip install alt-python-pynosqlc-cassandra`.
|
|
127
|
+
|
|
128
|
+
**`ValueError: No driver found for URL ...`**
|
|
129
|
+
The import `import pynosqlc.cassandra` was not executed before calling
|
|
130
|
+
`DriverManager.get_client(...)`. The import is what triggers driver
|
|
131
|
+
registration — it must come before any `get_client` call.
|
|
132
|
+
|
|
133
|
+
**`InvalidRequest: Keyspace '<name>' does not exist`**
|
|
134
|
+
This should not occur in normal use because the driver creates the keyspace
|
|
135
|
+
automatically. If you see this error, check that the connecting user has
|
|
136
|
+
`CREATE KEYSPACE` permission, or create the keyspace manually:
|
|
137
|
+
|
|
138
|
+
```cql
|
|
139
|
+
CREATE KEYSPACE my_keyspace
|
|
140
|
+
WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Filter returns no results despite documents being present**
|
|
144
|
+
Confirm you called `.build()` at the end of your filter chain:
|
|
145
|
+
`Filter.where('field').eq('value').build()`. Passing an unbuilt `FieldCondition`
|
|
146
|
+
object instead of the built `dict` will match nothing.
|
|
147
|
+
|
|
148
|
+
## Further Reading
|
|
149
|
+
|
|
150
|
+
- [pynosqlc API reference](../../docs/api-reference.md) — complete method signatures
|
|
151
|
+
- [Driver implementation guide](../../docs/driver-guide.md) — how pynosqlc drivers work
|
|
152
|
+
- [Getting started tutorial](../../docs/getting-started.md) — step-by-step introduction
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pynosqlc.cassandra — Cassandra driver for pynosqlc.
|
|
3
|
+
|
|
4
|
+
Handles URLs of the form: pynosqlc:cassandra:host:port/keyspace
|
|
5
|
+
|
|
6
|
+
Auto-registers ``CassandraDriver`` with ``DriverManager`` on import via
|
|
7
|
+
the ``cassandra_driver`` module.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pynosqlc.cassandra import cassandra_driver # noqa: F401
|
|
13
|
+
from pynosqlc.cassandra.cassandra_client import CassandraClient
|
|
14
|
+
from pynosqlc.cassandra.cassandra_collection import CassandraCollection
|
|
15
|
+
from pynosqlc.cassandra.cassandra_driver import CassandraDriver
|
|
16
|
+
|
|
17
|
+
__all__ = ["CassandraDriver", "CassandraClient", "CassandraCollection"]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cassandra_client.py — Cassandra Client implementation.
|
|
3
|
+
|
|
4
|
+
Each collection is created on demand; the parent Client base class
|
|
5
|
+
caches by name via get_collection().
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from pynosqlc.core.client import Client
|
|
14
|
+
from pynosqlc.cassandra.cassandra_collection import CassandraCollection
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from cassandra.cluster import Cluster, Session
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CassandraClient(Client):
|
|
21
|
+
"""Client backed by a cassandra-driver synchronous session.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
url: the pynosqlc URL used to open this connection
|
|
25
|
+
cluster: the connected ``Cluster`` instance (for shutdown)
|
|
26
|
+
session: the connected ``Session`` instance
|
|
27
|
+
keyspace: the active Cassandra keyspace
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
url: str,
|
|
33
|
+
cluster: "Cluster",
|
|
34
|
+
session: "Session",
|
|
35
|
+
keyspace: str,
|
|
36
|
+
) -> None:
|
|
37
|
+
super().__init__({"url": url})
|
|
38
|
+
self._cluster = cluster
|
|
39
|
+
self._session = session
|
|
40
|
+
self._keyspace = keyspace
|
|
41
|
+
|
|
42
|
+
def _get_collection(self, name: str) -> CassandraCollection:
|
|
43
|
+
"""Create and return a :class:`CassandraCollection` for *name*."""
|
|
44
|
+
return CassandraCollection(self, name, self._session)
|
|
45
|
+
|
|
46
|
+
async def _close(self) -> None:
|
|
47
|
+
"""Shut down the underlying Cassandra cluster connection."""
|
|
48
|
+
loop = asyncio.get_event_loop()
|
|
49
|
+
await loop.run_in_executor(None, self._cluster.shutdown)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cassandra_collection.py — Cassandra Collection implementation.
|
|
3
|
+
|
|
4
|
+
Storage layout
|
|
5
|
+
--------------
|
|
6
|
+
Each pynosqlc collection maps to one Cassandra table:
|
|
7
|
+
|
|
8
|
+
CREATE TABLE IF NOT EXISTS <name> (
|
|
9
|
+
pk TEXT PRIMARY KEY,
|
|
10
|
+
data TEXT
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
Documents are stored as JSON in the ``data`` column. Filtering is
|
|
14
|
+
performed in-process using :class:`MemoryFilterEvaluator` after a full
|
|
15
|
+
table scan — appropriate for test/dev workloads.
|
|
16
|
+
|
|
17
|
+
All session.execute() calls are dispatched via run_in_executor so they
|
|
18
|
+
don't block the asyncio event loop.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import json
|
|
25
|
+
import uuid
|
|
26
|
+
from typing import TYPE_CHECKING
|
|
27
|
+
|
|
28
|
+
from pynosqlc.core.collection import Collection
|
|
29
|
+
from pynosqlc.core.cursor import Cursor
|
|
30
|
+
from pynosqlc.memory.memory_filter_evaluator import MemoryFilterEvaluator
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from cassandra.cluster import Session
|
|
34
|
+
|
|
35
|
+
from pynosqlc.core.client import Client
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CassandraCollection(Collection):
|
|
39
|
+
"""Collection backed by a Cassandra table (pk TEXT, data TEXT).
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
client: the owning :class:`CassandraClient`
|
|
43
|
+
name: collection name (used as the CQL table name)
|
|
44
|
+
session: the shared cassandra-driver ``Session``
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, client: "Client", name: str, session: "Session") -> None:
|
|
48
|
+
super().__init__(client, name)
|
|
49
|
+
self._session = session
|
|
50
|
+
self._table_ready: bool = False
|
|
51
|
+
|
|
52
|
+
# ── Table bootstrap ────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
async def _ensure_table(self) -> None:
|
|
55
|
+
"""Create the backing table if it does not yet exist."""
|
|
56
|
+
if self._table_ready:
|
|
57
|
+
return
|
|
58
|
+
loop = asyncio.get_event_loop()
|
|
59
|
+
cql = (
|
|
60
|
+
f"CREATE TABLE IF NOT EXISTS {self._name} ("
|
|
61
|
+
f"pk TEXT PRIMARY KEY, data TEXT)"
|
|
62
|
+
)
|
|
63
|
+
await loop.run_in_executor(None, self._session.execute, cql)
|
|
64
|
+
self._table_ready = True
|
|
65
|
+
|
|
66
|
+
# ── Abstract implementation hooks ──────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
async def _get(self, key: str) -> dict | None:
|
|
69
|
+
await self._ensure_table()
|
|
70
|
+
loop = asyncio.get_event_loop()
|
|
71
|
+
cql = f"SELECT data FROM {self._name} WHERE pk = %s"
|
|
72
|
+
rows = await loop.run_in_executor(None, self._session.execute, cql, (key,))
|
|
73
|
+
row = rows.one()
|
|
74
|
+
return json.loads(row["data"]) if row is not None else None
|
|
75
|
+
|
|
76
|
+
async def _store(self, key: str, doc: dict) -> None:
|
|
77
|
+
await self._ensure_table()
|
|
78
|
+
loop = asyncio.get_event_loop()
|
|
79
|
+
cql = f"INSERT INTO {self._name} (pk, data) VALUES (%s, %s)"
|
|
80
|
+
await loop.run_in_executor(
|
|
81
|
+
None, self._session.execute, cql, (key, json.dumps(doc))
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
async def _delete(self, key: str) -> None:
|
|
85
|
+
await self._ensure_table()
|
|
86
|
+
loop = asyncio.get_event_loop()
|
|
87
|
+
cql = f"DELETE FROM {self._name} WHERE pk = %s"
|
|
88
|
+
await loop.run_in_executor(None, self._session.execute, cql, (key,))
|
|
89
|
+
|
|
90
|
+
async def _insert(self, doc: dict) -> str:
|
|
91
|
+
key = str(uuid.uuid4())
|
|
92
|
+
await self._store(key, {**doc, "_id": key})
|
|
93
|
+
return key
|
|
94
|
+
|
|
95
|
+
async def _update(self, key: str, patch: dict) -> None:
|
|
96
|
+
existing = await self._get(key)
|
|
97
|
+
if existing is None:
|
|
98
|
+
raise KeyError(f"Document not found for key: {key!r}")
|
|
99
|
+
await self._store(key, {**existing, **patch})
|
|
100
|
+
|
|
101
|
+
async def _find(self, ast: dict) -> Cursor:
|
|
102
|
+
await self._ensure_table()
|
|
103
|
+
loop = asyncio.get_event_loop()
|
|
104
|
+
cql = f"SELECT pk, data FROM {self._name}"
|
|
105
|
+
rows = await loop.run_in_executor(None, self._session.execute, cql)
|
|
106
|
+
|
|
107
|
+
results = []
|
|
108
|
+
for row in rows:
|
|
109
|
+
doc = json.loads(row["data"])
|
|
110
|
+
if MemoryFilterEvaluator.matches(doc, ast):
|
|
111
|
+
results.append(doc)
|
|
112
|
+
|
|
113
|
+
return Cursor(results)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cassandra_driver.py — Cassandra pynosqlc driver.
|
|
3
|
+
|
|
4
|
+
Handles URL: pynosqlc:cassandra:host:port/keyspace
|
|
5
|
+
Auto-registers with DriverManager on import.
|
|
6
|
+
|
|
7
|
+
cassandra-driver uses a synchronous API; all blocking calls are executed
|
|
8
|
+
via asyncio.get_event_loop().run_in_executor(None, ...) so they don't
|
|
9
|
+
block the event loop.
|
|
10
|
+
|
|
11
|
+
The default LibevConnection / ThreadedRequestExecutor reactor is used
|
|
12
|
+
(not AsyncioConnection) because AsyncioConnection cannot hook into the
|
|
13
|
+
running event loop when invoked from run_in_executor's thread-pool thread.
|
|
14
|
+
The standard reactor works correctly from any thread.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
|
|
21
|
+
from pynosqlc.core.driver import Driver
|
|
22
|
+
from pynosqlc.core.driver_manager import DriverManager
|
|
23
|
+
from pynosqlc.cassandra.cassandra_client import CassandraClient
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CassandraDriver(Driver):
|
|
27
|
+
"""Driver that creates :class:`CassandraClient` instances.
|
|
28
|
+
|
|
29
|
+
URL format: ``pynosqlc:cassandra:host:port/keyspace``
|
|
30
|
+
Default port: 9042
|
|
31
|
+
Default keyspace: ``pynosqlc``
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
URL_PREFIX: str = "pynosqlc:cassandra:"
|
|
35
|
+
|
|
36
|
+
def accepts_url(self, url: str) -> bool:
|
|
37
|
+
"""Return ``True`` for ``'pynosqlc:cassandra:...'`` URLs."""
|
|
38
|
+
return isinstance(url, str) and url.startswith(self.URL_PREFIX)
|
|
39
|
+
|
|
40
|
+
async def connect(
|
|
41
|
+
self,
|
|
42
|
+
url: str,
|
|
43
|
+
properties: dict | None = None,
|
|
44
|
+
) -> CassandraClient:
|
|
45
|
+
"""Parse URL, connect, and return a :class:`CassandraClient`.
|
|
46
|
+
|
|
47
|
+
URL format: ``pynosqlc:cassandra:host:port/keyspace``
|
|
48
|
+
Example: ``pynosqlc:cassandra:localhost:9042/pynosqlc_test``
|
|
49
|
+
|
|
50
|
+
Connection errors from cassandra-driver propagate directly —
|
|
51
|
+
``NoHostAvailable`` is the expected signal when Cassandra is absent
|
|
52
|
+
(used by compliance tests to skip).
|
|
53
|
+
"""
|
|
54
|
+
from cassandra.cluster import Cluster
|
|
55
|
+
from cassandra.query import dict_factory
|
|
56
|
+
|
|
57
|
+
# Parse: strip prefix → "host:port/keyspace"
|
|
58
|
+
tail = url[len(self.URL_PREFIX):]
|
|
59
|
+
if "/" in tail:
|
|
60
|
+
host_port, keyspace = tail.split("/", 1)
|
|
61
|
+
else:
|
|
62
|
+
host_port, keyspace = tail, "pynosqlc"
|
|
63
|
+
|
|
64
|
+
if ":" in host_port:
|
|
65
|
+
host, port_str = host_port.rsplit(":", 1)
|
|
66
|
+
port = int(port_str)
|
|
67
|
+
else:
|
|
68
|
+
host = host_port
|
|
69
|
+
port = 9042
|
|
70
|
+
|
|
71
|
+
loop = asyncio.get_event_loop()
|
|
72
|
+
|
|
73
|
+
cluster = Cluster(
|
|
74
|
+
contact_points=[host],
|
|
75
|
+
port=port,
|
|
76
|
+
)
|
|
77
|
+
session = await loop.run_in_executor(None, lambda: cluster.connect())
|
|
78
|
+
await loop.run_in_executor(
|
|
79
|
+
None,
|
|
80
|
+
session.execute,
|
|
81
|
+
(
|
|
82
|
+
f"CREATE KEYSPACE IF NOT EXISTS {keyspace} "
|
|
83
|
+
f"WITH REPLICATION = {{'class': 'SimpleStrategy', 'replication_factor': 1}}"
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
await loop.run_in_executor(None, session.set_keyspace, keyspace)
|
|
87
|
+
session.row_factory = dict_factory
|
|
88
|
+
|
|
89
|
+
return CassandraClient(url, cluster, session, keyspace)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Auto-register on import — a single shared instance is sufficient.
|
|
93
|
+
_driver = CassandraDriver()
|
|
94
|
+
DriverManager.register_driver(_driver)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "alt-python-pynosqlc-cassandra"
|
|
3
|
+
version = "1.0.4"
|
|
4
|
+
description = "Cassandra driver for pynosqlc"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"alt-python-pynosqlc-core",
|
|
9
|
+
"alt-python-pynosqlc-memory",
|
|
10
|
+
"cassandra-driver>=3.29.1",
|
|
11
|
+
]
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Craig Parravicini"},
|
|
14
|
+
{name = "Claude (Anthropic)"},
|
|
15
|
+
]
|
|
16
|
+
license = {text = "MIT"}
|
|
17
|
+
keywords = ["nosql", "database", "async", "cassandra", "driver"]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 5 - Production/Stable",
|
|
20
|
+
"Framework :: AsyncIO",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Database",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/alt-python/pynosqlc"
|
|
31
|
+
Repository = "https://github.com/alt-python/pynosqlc"
|
|
32
|
+
Documentation = "https://github.com/alt-python/pynosqlc#getting-started"
|
|
33
|
+
"Bug Tracker" = "https://github.com/alt-python/pynosqlc/issues"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["hatchling"]
|
|
37
|
+
build-backend = "hatchling.build"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["pynosqlc"]
|
|
41
|
+
|
|
42
|
+
[tool.uv.sources]
|
|
43
|
+
alt-python-pynosqlc-core = { workspace = true }
|
|
44
|
+
alt-python-pynosqlc-memory = { workspace = true }
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
asyncio_mode = "auto"
|
|
49
|
+
addopts = "--import-mode=importlib"
|
|
File without changes
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
test_compliance.py — Cassandra driver compliance tests.
|
|
3
|
+
|
|
4
|
+
Wires the shared pynosqlc.core compliance suite into the cassandra package.
|
|
5
|
+
Each test run gets a fresh CassandraClient connected to a live Cassandra 4 instance.
|
|
6
|
+
|
|
7
|
+
Set CASSANDRA_URL to override the default URL
|
|
8
|
+
(default: pynosqlc:cassandra:localhost:9042/pynosqlc_test).
|
|
9
|
+
Tests are skipped automatically if Cassandra is not reachable.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
from pynosqlc.core import DriverManager
|
|
20
|
+
from pynosqlc.core.testing import run_compliance
|
|
21
|
+
import pynosqlc.cassandra # noqa: F401 — registers CassandraDriver on import
|
|
22
|
+
from pynosqlc.cassandra.cassandra_driver import _driver
|
|
23
|
+
|
|
24
|
+
CASSANDRA_URL = os.environ.get(
|
|
25
|
+
"CASSANDRA_URL", "pynosqlc:cassandra:localhost:9042/pynosqlc_test"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def _factory():
|
|
30
|
+
"""Return a fresh, open CassandraClient for each test class fixture.
|
|
31
|
+
|
|
32
|
+
Clears and re-registers the driver, connects to Cassandra, drops and
|
|
33
|
+
recreates the keyspace so each test class starts with a clean slate,
|
|
34
|
+
and returns the client.
|
|
35
|
+
|
|
36
|
+
Skips the test if Cassandra is not reachable.
|
|
37
|
+
"""
|
|
38
|
+
DriverManager.clear()
|
|
39
|
+
DriverManager.register_driver(_driver)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
client = await DriverManager.get_client(CASSANDRA_URL)
|
|
43
|
+
# Drop and recreate keyspace for a clean slate between test classes
|
|
44
|
+
loop = asyncio.get_event_loop()
|
|
45
|
+
keyspace = "pynosqlc_test"
|
|
46
|
+
await loop.run_in_executor(
|
|
47
|
+
None,
|
|
48
|
+
client._session.execute,
|
|
49
|
+
f"DROP KEYSPACE IF EXISTS {keyspace}",
|
|
50
|
+
)
|
|
51
|
+
await loop.run_in_executor(
|
|
52
|
+
None,
|
|
53
|
+
client._session.execute,
|
|
54
|
+
(
|
|
55
|
+
f"CREATE KEYSPACE IF NOT EXISTS {keyspace} "
|
|
56
|
+
f"WITH REPLICATION = {{'class': 'SimpleStrategy', 'replication_factor': 1}}"
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
await loop.run_in_executor(None, client._session.set_keyspace, keyspace)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
pytest.skip(f"Cassandra not available: {e}")
|
|
62
|
+
|
|
63
|
+
return client
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
run_compliance(_factory)
|