7.2.6 - JsonPatch, Queue Sink, CDC Sink & Smuggler ππ
7.2.6 syncs the Python client with the RavenDB 7.2.5 and 7.2.6 C# clients in one go, and it is a big one. The session patch methods now build RFC 6902 JsonPatch commands instead of JavaScript patch scripts. Three subsystems the Python client simply never had arrive complete: Queue Sink, CDC Sink and the database smuggler. Server-wide connection strings, SSO on client certificates and per-turn output schemas for AI agents land alongside them. And 269 types that existed but were only reachable through their module paths are now importable straight from ravendb.
- JSON Patch: https://ravendb.net/docs/article-page/latest/python/client-api/operations/patching/json-patch-syntax
- Queue Sink: https://ravendb.net/docs/article-page/latest/python/server/ongoing-tasks/queue-sink/overview
- CDC Sink: https://ravendb.net/docs/article-page/latest/python/server/ongoing-tasks/cdc-sink/overview
- Smuggler: https://ravendb.net/docs/article-page/latest/python/client-api/smuggler/what-is-smuggler
- Connection strings: https://ravendb.net/docs/article-page/latest/python/client-api/operations/maintenance/connection-strings/add-connection-string
- Certificate management: https://ravendb.net/docs/article-page/latest/python/server/security/authentication/certificate-management
- PyPI: https://pypi.org/project/ravendb/7.2.6/
Highlights
Session patching with JsonPatch
session.advanced.patch, patch_array and patch_object now emit an RFC 6902 JsonPatch command wherever the path and the value allow it, so the server applies the change structurally instead of running a patch script. Anything JsonPatch cannot express keeps the old JavaScript path automatically: composite values, remove_all predicates, increment, and paths that are not addressable as a JSON pointer. Patches for the same document merge into a single command, as they always did.
from ravendb import DocumentStore
store = DocumentStore(urls=["http://localhost:8080"], database="NorthWind")
store.initialize()
with store.open_session() as session:
session.advanced.patch("users/1-A", "Name", "patched") # add /Name
session.advanced.patch("users/1-A", "Address.City", "Krakow") # add /Address/City
session.advanced.patch("users/1-A", "Tags[0]", "primary") # replace /Tags/0
session.advanced.patch_array("users/1-A", "Tags", lambda tags: tags.add("new"))
session.save_changes() # one JsonPatch command, four operationsThe standalone operation is available too, for building a patch by hand:
from ravendb import JsonPatchDocument, JsonPatchOperation
patch = JsonPatchDocument().add("/Name", "patched").add("/Tags/-", "c").replace("/Tags/0", "z")
result = store.operations.send(JsonPatchOperation("users/1-A", patch))Set store.conventions.session_patch_behavior = SessionPatchBehavior.JAVA_SCRIPT before initialize() to stay on the previous code path entirely.
New operations: JsonPatchOperation
New classes: JsonPatchDocument, JsonPatchResult, JsonPatchCommandData
New enums: SessionPatchBehavior (JSON_PATCH default, JAVA_SCRIPT), CommandType.JSON_PATCH
New convention: DocumentConventions.session_patch_behavior
Queue Sink
RavenDB consuming messages from Kafka, RabbitMQ, Azure Queue Storage, Amazon SQS or Azure Service Bus. The client had the QueueSink ongoing-task enum member and nothing behind it: no configuration, no operations, no task result. It now has all of it, including DatabaseRecord.queue_sinks.
from ravendb import (
AddQueueSinkOperation, AzureServiceBusSinkSource,
QueueBrokerType, QueueSinkConfiguration, QueueSinkScript,
)
store.maintenance.send(AddQueueSinkOperation(QueueSinkConfiguration(
name="orders-sink",
broker_type=QueueBrokerType.AZURE_SERVICE_BUS,
connection_string_name="asb",
scripts=[QueueSinkScript("orders", [
AzureServiceBusSinkSource.queue("orders"),
AzureServiceBusSinkSource.subscription("events", "audit"),
], "put('orders/', this);")],
)))New operations: AddQueueSinkOperation, UpdateQueueSinkOperation
New classes: QueueSinkConfiguration, QueueSinkScript, AzureServiceBusSinkSource, QueueSinkProcessState, OngoingTaskQueueSink, AddQueueSinkOperationResult, UpdateQueueSinkOperationResult
CDC Sink
The whole public configuration tree (13 model types, from CdcColumnMapping up to CdcSinkConfiguration, with embedded and linked tables nesting to any depth), plus the two task operations, the ongoing-task result with its lag and health fields, and DatabaseRecord.cdc_sinks.
from ravendb import (
AddCdcSinkOperation, CdcColumnMapping, CdcColumnType, CdcSinkConfiguration,
CdcSinkOnDeleteConfig, CdcSinkPostgresSettings, CdcSinkTableConfig,
)
store.maintenance.send(AddCdcSinkOperation(CdcSinkConfiguration(
name="orders-cdc", connection_string_name="pg",
postgres=CdcSinkPostgresSettings(publication_name="pub", slot_name="slot"),
tables=[CdcSinkTableConfig(
collection_name="Orders", source_table_name="orders",
columns=[CdcColumnMapping("payload", "Payload", CdcColumnType.JSON)],
primary_key_columns=["id"],
on_delete=CdcSinkOnDeleteConfig(ignore_deletes=True))],
)))Schema discovery and mapping preview come with it: GetCdcSinkSchemaOperation browses the source database a CDC task would read: tables, columns, primary and foreign keys, each annotated with what CDC can actually capture. TestCdcSinkMappingOperation runs real rows through the configured mapping without saving anything.
New operations: AddCdcSinkOperation, UpdateCdcSinkOperation, GetCdcSinkSchemaOperation, TestCdcSinkMappingOperation
New classes: CdcSinkConfiguration, CdcSinkTableConfig, CdcSinkEmbeddedTableConfig, CdcSinkLinkedTableConfig, CdcSinkOnDeleteConfig, CdcSinkPostgresSettings, CdcColumnMapping, OngoingTaskCdcSink
New enums: CdcColumnType, CdcSinkRelationType, CdcSinkProcessState, CdcSinkTaskState, CdcSinkTableLoadState, OngoingTaskType.CDC_SINK
Database smuggler
store.smuggler exports a database to a file or a stream and imports one back. Item selection is a set of DatabaseItemType members, rendered to the comma-separated string the server parses.
from ravendb import DatabaseItemType, DatabaseSmugglerExportOptions, DatabaseSmugglerImportOptions
store.smuggler.export(DatabaseSmugglerExportOptions(
operate_on_types={DatabaseItemType.DOCUMENTS},
collections=["Orders"]), "dump.ravendbdump").wait_for_completion()
other.smuggler.import_data(DatabaseSmugglerImportOptions(), "dump.ravendbdump")
store.smuggler.for_database("reporting").import_incremental(options, backup_dir)Results are typed. A finished smuggler operation hands back a SmugglerResult with counts per item type, rather than a raw dictionary:
result = store.smuggler.import_data(options, "dump.ravendbdump").wait_for_completion()
result.documents.read_count
result.documents.attachments.read_count
result.database_record.updatedNew property: store.smuggler
New classes: DatabaseSmuggler, DatabaseSmugglerExportOptions, DatabaseSmugglerImportOptions, DatabaseSmugglerOptions, SmugglerResult, SmugglerProgressBase, DatabaseRecordProgress, SmugglerOperation
New enums: DatabaseItemType, DatabaseRecordItemType, ExportCompressionAlgorithm
Server-wide connection strings
A connection string the cluster propagates to every database except the ones you exclude. ServerWideConnectionString flattens the underlying connection string into the request body, the shape the endpoint expects, and dispatches back to the right concrete type on read.
from ravendb import RavenConnectionString
from ravendb.serverwide.operations.connection_strings import (
PutServerWideConnectionStringOperation, ServerWideConnectionString,
)
store.maintenance.server.send(PutServerWideConnectionStringOperation(
ServerWideConnectionString(
connection_string=RavenConnectionString("shared", "orders", ["http://localhost:8080"]),
excluded_databases=["scratch"])))
# the name a propagated connection string carries inside a database record
ServerWideConnectionString.get_database_record_connection_string_name("shared")
# -> "Server Wide Connection String, shared"New operations: PutServerWideConnectionStringOperation, GetServerWideConnectionStringsOperation, RemoveServerWideConnectionStringOperation
New classes: ServerWideConnectionString, ConnectionStringUsage
New enum: ConnectionStringUsageKind
AI agents: per-turn output schema
An agent's output schema is fixed when the agent is created. A single conversation turn can now override it without disturbing the turns around it.
from ravendb import AiOutputOptions
chat = store.ai.conversation("agents/support", "conversations/")
chat.set_user_prompt("How does the customer feel about the delay?")
# a schema derived from a sample object, for this turn only
chat.run_with_schema(AiOutputOptions(sample_object={"Sentiment": "positive"}))
# an explicit JSON schema, or no structure at all so the model answers in prose
chat.run_with_schema(AiOutputOptions(output_schema=schema))
chat.run_with_schema(AiOutputOptions(no_schema=True))C# splits these across a dozen generic overloads keyed on TAnswer; Python has one options object and two entry points. Asking for no schema while also supplying one is refused.
New classes: AiOutputOptions
New methods: AiConversation.run_with_schema, AiConversation.stream_with_schema
AI agents: reading a conversation back
store.ai.get_conversation_messages(...) takes either a conversation id or a GetConversationMessagesOptions for timestamp paging and a detail level. Messages come back with roles, inlined tool calls and per-message usage.
from ravendb import AiConversationDetailLevel, GetConversationMessagesOptions
messages = store.ai.get_conversation_messages(GetConversationMessagesOptions(
"chats/1-A", page_size=25, detail_level=AiConversationDetailLevel.DETAILED))
for message in messages.messages:
print(message.role, message.content)New operations: GetConversationMessagesOperation
New classes: GetConversationMessagesOptions, AiConversationMessage, AiConversationMessagesResult, AiToolCallResult
New enums: AiConversationDetailLevel, AiMessageRole
SSO on client certificates
CertificateMetadata gains usage, sso_identifiers, sso_server_public_key_pinning_hashes and allow_any_sso_server. The interesting part is the write side: EditClientCertificateOperation now builds its body explicitly, so the three SSO fields are only sent when you set them. Leave them unset and the server keeps the stored SSO configuration, which is what a plain rename or a disabled-only toggle on an SSO user wants. Pass an empty list and it clears the stored value.
from ravendb import EditClientCertificateOperation, SecurityClearance, SsoIdentifier, SsoProvider
store.maintenance.server.send(EditClientCertificateOperation(
EditClientCertificateOperation.Parameters(
thumbprint=thumbprint, name="reporting", clearance=SecurityClearance.OPERATOR,
permissions={}, sso_identifiers=[SsoIdentifier(SsoProvider.MICROSOFT, "user@example.com")])))New classes: SsoIdentifier
New enums: SsoProvider, CertificateUsage
New fields on CertificateMetadata, new opt-in parameters on EditClientCertificateOperation.Parameters
Typed license limits
A refusal on licensing grounds used to arrive as a bare RavenException you had to string-match. It now arrives as LicenseLimitException, registered in the exception dispatcher, alongside the full LimitType enum (59 members, including ServerWideConnectionStrings, CdcSink and Sso). It derives from RavenException, so existing handlers keep working.
from ravendb import AddCdcSinkOperation, LicenseLimitException
try:
store.maintenance.send(AddCdcSinkOperation(configuration))
except LicenseLimitException as error:
print("not available on this license:", error)New exceptions: LicenseLimitException
New enums: LimitType
The whole public surface is importable from ravendb
ravendb/__init__.py carried a long backlog of types that existed but were never exported, so whole subsystems could only be reached through their module paths. 269 names are now importable straight from the package root: subscriptions, time series, counters, the changes API, index and spatial enums, the exception hierarchy, ETL connection strings for every queue broker, and the server-wide operations.
# all of these used to need their full module path
from ravendb import (
SubscriptionCreationOptions, SubscriptionWorkerOptions, # subscriptions
TimeSeriesRange, ConfigureTimeSeriesOperation, # time series
CounterBatchOperation, DocumentChange, DatabaseChanges, # counters, changes
FieldIndexing, FieldStorage, SpatialOptions, # index definitions
ConcurrencyException, RavenException, # exceptions
DeleteDatabaseOperation, GetLogsConfigurationOperation, # server-wide
KafkaConnectionSettings, RabbitMqConnectionSettings, # ETL connection strings
)test_imports.py now asserts every name the package root binds, so an export cannot be dropped without failing the suite.
Other Changes
- Azure Service Bus as a queue ETL broker -
QueueBrokerType.AZURE_SERVICE_BUSandAzureServiceBusConnectionSettings, with all three authentication shapes: a connection string, Entra ID service-principal credentials (AzureServiceBusEntraId), or passwordless managed identity (AzureServiceBusPasswordless). - Cloud settings conversions -
S3Settings.to_remote_attachments_s3_settings()andRemoteAttachmentsS3Settings.to_s3_settings(), and the same pair for Azure. BackupS3Settingsgainsstorage_class, which it was missing entirely. - S3 checksum opt-out -
disable_checksum_validationon bothS3SettingsandRemoteAttachmentsS3Settings, for S3-compatible storage without modern object integrity checks. - Chunk text on embeddings generation -
EmbeddingsGenerationConfiguration.store_chunk_textkeeps each chunk's text next to its embedding. - A typed query-tool failure -
QueryToolFailedExceptionjoins theAiExceptionfamily and the dispatcher, so an agent's failed query tool is catchable by type. - Cancel pending action tools -
AiConversation.cancel_pending_action_toolsdrops the tool calls a conversation is still waiting on instead of answering them; the flag clears once a run succeeds. - Pull replication cursors -
hub_cursorandsink_cursoronOngoingTaskPullReplicationAsSink. - Connection string usages - the GET endpoint returns a
UsedByarray on every connection string; it is parsed intoconnection_string.used_byinstead of being dropped. - A vector search survives a query alias -
VectorSearchTokeninheritedadd_aliasfromWhereToken, so qualifying a field with a query alias threw away the similarity threshold, the candidate count and exactness. Chasing that turned up an older problem: the caller rantoken.add_alias(from_alias)and discarded the result, so no alias was ever applied. DatabaseRecorddeserialization -from_jsoncalled.items()on a possibly-absentAutoIndexesand passed a possibly-absentLockModestraight into an enum, so any payload missing either raised.to_jsonhad the mirror bug and could never serialize an auto-index.AutoIndexDefinition.from_jsonhad the same strictness one level down.- Constants - the
X-Forwarded-Forheader and three newSupportedFeatureskeys. RequestExecutor.CLIENT_VERSIONis now7.2.6.
Compatibility
Every signature change in this release is additive. Four constructors that would have taken a new parameter mid-list (RunConversationOperation, EmbeddingsGenerationConfiguration and RemoteAttachmentsS3Settings) take it at the end instead, so positional callers are unaffected, and Operation.wait_for_completion() still returns None.
The one behaviour change worth knowing about is the session patch default. Measured against a live server, a JsonPatch and the equivalent script are identical for setting an existing member, creating a new one, overwriting an array element, and writing through a missing intermediate (both error). They differ in exactly one case: writing past the end of an array, which the script appends and JsonPatch rejects. SessionPatchBehavior.JAVA_SCRIPT restores the previous path wholesale.