0
# Array Operations
1
2
## Overview
3
4
The `Array` type in pycrdt provides collaborative array/list functionality with automatic conflict resolution across multiple clients. It supports a complete list-like interface with additional collaborative features like move operations, change tracking, and type-safe variants.
5
6
## Core Types
7
8
### Array
9
10
Collaborative array with list-like interface and change tracking.
11
12
```python { .api }
13
class Array[T]:
14
def __init__(
15
self,
16
init: list[T] | None = None,
17
*,
18
_doc: Doc | None = None,
19
_integrated: _Array | None = None,
20
) -> None:
21
"""
22
Create a new collaborative array.
23
24
Args:
25
init (list, optional): Initial array contents
26
_doc (Doc, optional): Parent document
27
_integrated (_Array, optional): Native array instance
28
"""
29
30
# List-like interface
31
def __len__(self) -> int:
32
"""Get the length of the array."""
33
34
def __str__(self) -> str:
35
"""Get string representation of the array."""
36
37
def __iter__(self) -> ArrayIterator:
38
"""Iterate over array elements."""
39
40
def __contains__(self, item: T) -> bool:
41
"""Check if item exists in array."""
42
43
def __getitem__(self, key: int | slice) -> T | list[T]:
44
"""Get element or slice by index."""
45
46
def __setitem__(self, key: int | slice, value: T | list[T]) -> None:
47
"""Set element or slice by index."""
48
49
def __delitem__(self, key: int | slice) -> None:
50
"""Delete element or slice by index."""
51
52
def __add__(self, value: list[T]) -> Array[T]:
53
"""Concatenate with another list."""
54
55
def __radd__(self, value: list[T]) -> Array[T]:
56
"""Right-side concatenation with another list."""
57
58
# Array manipulation methods
59
def append(self, value: T) -> None:
60
"""
61
Append an element to the end of the array.
62
63
Args:
64
value: Element to append
65
"""
66
67
def extend(self, value: list[T]) -> None:
68
"""
69
Extend the array with elements from an iterable.
70
71
Args:
72
value (list): Elements to add to the array
73
"""
74
75
def insert(self, index: int, object: T) -> None:
76
"""
77
Insert an element at the specified index.
78
79
Args:
80
index (int): Position to insert element
81
object: Element to insert
82
"""
83
84
def pop(self, index: int = -1) -> T:
85
"""
86
Remove and return element at index (default last).
87
88
Args:
89
index (int): Index of element to remove
90
91
Returns:
92
T: Removed element
93
"""
94
95
def move(self, source_index: int, destination_index: int) -> None:
96
"""
97
Move an element from source to destination index.
98
99
Args:
100
source_index (int): Current position of element
101
destination_index (int): New position for element
102
"""
103
104
def clear(self) -> None:
105
"""Remove all elements from the array."""
106
107
def to_py(self) -> list[T] | None:
108
"""
109
Convert array to a Python list.
110
111
Returns:
112
list | None: Array contents as list, or None if empty
113
"""
114
115
def observe(self, callback: Callable[[ArrayEvent], None]) -> Subscription:
116
"""
117
Observe array changes.
118
119
Args:
120
callback: Function called when array changes occur
121
122
Returns:
123
Subscription: Handle for unsubscribing
124
"""
125
126
def observe_deep(self, callback: Callable[[list[ArrayEvent]], None]) -> Subscription:
127
"""
128
Observe deep changes including nested structures.
129
130
Args:
131
callback: Function called with list of change events
132
133
Returns:
134
Subscription: Handle for unsubscribing
135
"""
136
137
def unobserve(self, subscription: Subscription) -> None:
138
"""
139
Remove an event observer.
140
141
Args:
142
subscription: Subscription handle to remove
143
"""
144
145
async def events(
146
self,
147
deep: bool = False,
148
max_buffer_size: float = float("inf")
149
) -> MemoryObjectReceiveStream:
150
"""
151
Get an async stream of array events.
152
153
Args:
154
deep (bool): Include deep change events
155
max_buffer_size (float): Maximum event buffer size
156
157
Returns:
158
MemoryObjectReceiveStream: Async event stream
159
"""
160
161
def sticky_index(self, index: int, assoc: Assoc = Assoc.AFTER) -> StickyIndex:
162
"""
163
Create a sticky index that maintains its position during edits.
164
165
Args:
166
index (int): Initial index position
167
assoc (Assoc): Association type (BEFORE or AFTER)
168
169
Returns:
170
StickyIndex: Persistent position tracker
171
"""
172
```
173
174
### ArrayEvent
175
176
Event emitted when array changes occur.
177
178
```python { .api }
179
class ArrayEvent:
180
@property
181
def target(self) -> Array:
182
"""Get the array that changed."""
183
184
@property
185
def delta(self) -> list[dict[str, Any]]:
186
"""
187
Get the delta describing the changes.
188
189
Delta format:
190
- {"retain": n} - Keep n elements unchanged
191
- {"insert": [items]} - Insert elements
192
- {"delete": n} - Delete n elements
193
"""
194
195
@property
196
def path(self) -> list[int | str]:
197
"""Get the path to the changed array within the document structure."""
198
```
199
200
### TypedArray
201
202
Type-safe wrapper for Array with typed elements.
203
204
```python { .api }
205
class TypedArray[T]:
206
"""
207
Type-safe array container with runtime type checking.
208
209
Usage:
210
class StringArray(TypedArray[str]):
211
type = str # Define element type
212
213
array = StringArray()
214
array.append("hello") # Type-safe
215
item: str = array[0] # Typed access
216
"""
217
```
218
219
## Usage Examples
220
221
### Basic Array Operations
222
223
```python
224
from pycrdt import Doc, Array
225
226
doc = Doc()
227
array = doc.get("items", type=Array)
228
229
# Basic list operations
230
array.append("item1")
231
array.append("item2")
232
array.extend(["item3", "item4"])
233
print(len(array)) # 4
234
235
# List-like access
236
print(array[0]) # "item1"
237
print(array[-1]) # "item4"
238
print(array[1:3]) # ["item2", "item3"]
239
240
# Modification
241
array[1] = "modified_item2"
242
array.insert(2, "inserted_item")
243
244
# Check contents
245
print("item1" in array) # True
246
print(list(array)) # All elements
247
```
248
249
### Array Manipulation
250
251
```python
252
from pycrdt import Doc, Array
253
254
doc = Doc()
255
numbers = doc.get("numbers", type=Array)
256
257
# Build array
258
numbers.extend([1, 2, 3, 4, 5])
259
260
# Move operations (unique to collaborative arrays)
261
numbers.move(0, 4) # Move first element to end
262
print(list(numbers)) # [2, 3, 4, 5, 1]
263
264
# Pop operations
265
last = numbers.pop() # Remove and return last element
266
first = numbers.pop(0) # Remove and return first element
267
print(f"Removed: {first}, {last}")
268
269
# Slice operations
270
numbers[1:3] = [10, 20, 30] # Replace slice
271
del numbers[2:4] # Delete slice
272
```
273
274
### Nested Data Structures
275
276
```python
277
from pycrdt import Doc, Array, Map
278
279
doc = Doc()
280
users = doc.get("users", type=Array)
281
282
# Add user objects
283
user1 = Map()
284
user1["name"] = "Alice"
285
user1["age"] = 30
286
users.append(user1)
287
288
user2 = Map()
289
user2["name"] = "Bob"
290
user2["age"] = 25
291
users.append(user2)
292
293
# Access nested data
294
print(users[0]["name"]) # "Alice"
295
print(users[1]["age"]) # 25
296
297
# Modify nested structures
298
users[0]["age"] = 31
299
print(users[0]["age"]) # 31
300
```
301
302
### Type-Safe Arrays
303
304
```python
305
from pycrdt import TypedArray, Doc
306
307
class NumberList(TypedArray[int]):
308
type = int
309
310
class StringList(TypedArray[str]):
311
type = str
312
313
doc = Doc()
314
315
# Create typed arrays
316
numbers = NumberList()
317
strings = StringList()
318
319
# Type-safe operations
320
numbers.append(42) # OK
321
strings.append("hello") # OK
322
323
try:
324
numbers.append("string") # May raise TypeError
325
except TypeError as e:
326
print(f"Type error: {e}")
327
328
# Typed access
329
first_number: int = numbers[0] # Typed
330
first_string: str = strings[0] # Typed
331
```
332
333
### Position Tracking
334
335
```python
336
from pycrdt import Doc, Array, Assoc
337
338
doc = Doc()
339
tasks = doc.get("tasks", type=Array)
340
341
tasks.extend(["Task 1", "Task 2", "Task 3", "Task 4"])
342
343
# Create sticky indices
344
important_pos = tasks.sticky_index(1, Assoc.BEFORE) # Before "Task 2"
345
end_pos = tasks.sticky_index(3, Assoc.AFTER) # After "Task 3"
346
347
# Insert elements
348
tasks.insert(0, "Urgent Task")
349
tasks.append("Final Task")
350
351
# Check positions (they maintain relative positions)
352
with doc.transaction() as txn:
353
important_idx = important_pos.get_index(txn)
354
end_idx = end_pos.get_index(txn)
355
print(f"Important task at: {important_idx}") # Adjusted index
356
print(f"End position at: {end_idx}") # Adjusted index
357
```
358
359
### Event Observation
360
361
```python
362
from pycrdt import Doc, Array, ArrayEvent
363
364
doc = Doc()
365
array = doc.get("items", type=Array)
366
367
def on_array_change(event: ArrayEvent):
368
print(f"Array changed: {event.target}")
369
print(f"Delta: {event.delta}")
370
for op in event.delta:
371
if "retain" in op:
372
print(f" Retain {op['retain']} elements")
373
elif "insert" in op:
374
items = op["insert"]
375
print(f" Insert {len(items)} elements: {items}")
376
elif "delete" in op:
377
print(f" Delete {op['delete']} elements")
378
379
# Subscribe to changes
380
subscription = array.observe(on_array_change)
381
382
# Make changes to trigger events
383
array.append("item1")
384
array.extend(["item2", "item3"])
385
array.move(0, 2)
386
array.pop()
387
388
# Clean up
389
array.unobserve(subscription)
390
```
391
392
### Async Event Streaming
393
394
```python
395
import anyio
396
from pycrdt import Doc, Array
397
398
async def monitor_array_changes(array: Array):
399
async with array.events() as event_stream:
400
async for event in event_stream:
401
print(f"Array event: {event.delta}")
402
403
doc = Doc()
404
array = doc.get("items", type=Array)
405
406
async def main():
407
async with anyio.create_task_group() as tg:
408
tg.start_soon(monitor_array_changes, array)
409
410
# Make changes
411
await anyio.sleep(0.1)
412
array.append("item1")
413
await anyio.sleep(0.1)
414
array.extend(["item2", "item3"])
415
await anyio.sleep(0.1)
416
417
anyio.run(main)
418
```
419
420
### Collaborative Array Editing
421
422
```python
423
from pycrdt import Doc, Array
424
425
# Simulate two clients editing the same array
426
doc1 = Doc(client_id=1)
427
doc2 = Doc(client_id=2)
428
429
array1 = doc1.get("shared_list", type=Array)
430
array2 = doc2.get("shared_list", type=Array)
431
432
# Client 1 adds items
433
with doc1.transaction(origin="client1"):
434
array1.extend([1, 2, 3])
435
436
# Sync to client 2
437
update = doc1.get_update()
438
doc2.apply_update(update)
439
print(list(array2)) # [1, 2, 3]
440
441
# Client 2 makes concurrent changes
442
with doc2.transaction(origin="client2"):
443
array2.insert(0, 0) # Insert at beginning
444
array2.append(4) # Add to end
445
array2.move(2, 0) # Move element
446
447
# Sync back to client 1
448
update = doc2.get_update(doc1.get_state())
449
doc1.apply_update(update)
450
451
# Both clients now have the same state
452
print(f"Client 1: {list(array1)}")
453
print(f"Client 2: {list(array2)}")
454
```
455
456
### Complex Data Processing
457
458
```python
459
from pycrdt import Doc, Array, Map
460
461
doc = Doc()
462
inventory = doc.get("inventory", type=Array)
463
464
# Build complex inventory data
465
items = [
466
{"name": "Widget A", "price": 10.99, "quantity": 50},
467
{"name": "Widget B", "price": 15.99, "quantity": 30},
468
{"name": "Widget C", "price": 8.99, "quantity": 75},
469
]
470
471
for item_data in items:
472
item = Map()
473
for key, value in item_data.items():
474
item[key] = value
475
inventory.append(item)
476
477
# Process inventory
478
def calculate_total_value(inventory: Array) -> float:
479
"""Calculate total inventory value."""
480
total = 0.0
481
for item in inventory:
482
price = item["price"]
483
quantity = item["quantity"]
484
total += price * quantity
485
return total
486
487
print(f"Total inventory value: ${calculate_total_value(inventory):.2f}")
488
489
# Update quantities
490
def update_quantity(inventory: Array, name: str, new_quantity: int):
491
"""Update quantity for a specific item."""
492
for item in inventory:
493
if item["name"] == name:
494
item["quantity"] = new_quantity
495
break
496
497
update_quantity(inventory, "Widget A", 45)
498
print(f"Updated inventory value: ${calculate_total_value(inventory):.2f}")
499
500
# Add new items
501
new_item = Map()
502
new_item["name"] = "Widget D"
503
new_item["price"] = 12.99
504
new_item["quantity"] = 20
505
inventory.append(new_item)
506
```
507
508
### Array Sorting and Filtering
509
510
```python
511
from pycrdt import Doc, Array
512
513
doc = Doc()
514
numbers = doc.get("numbers", type=Array)
515
numbers.extend([3, 1, 4, 1, 5, 9, 2, 6])
516
517
# Sort array (collaborative way)
518
def collaborative_sort(array: Array, key=None, reverse=False):
519
"""Sort array in place using collaborative moves."""
520
# Get current elements
521
elements = list(array)
522
523
# Get sorted indices
524
sorted_indices = sorted(range(len(elements)),
525
key=lambda i: elements[i] if key is None else key(elements[i]),
526
reverse=reverse)
527
528
# Apply moves to achieve sorted order
529
for target_pos, source_pos in enumerate(sorted_indices):
530
if source_pos != target_pos:
531
# Find current position of the element we want to move
532
current_pos = source_pos
533
for i, idx in enumerate(sorted_indices[:target_pos]):
534
if idx < source_pos:
535
current_pos -= 1
536
537
if current_pos != target_pos:
538
array.move(current_pos, target_pos)
539
540
# Sort the array
541
collaborative_sort(numbers)
542
print(f"Sorted: {list(numbers)}")
543
544
# Filter and rebuild (non-collaborative approach)
545
def filter_array(array: Array, predicate) -> Array:
546
"""Create new array with filtered elements."""
547
filtered = Array()
548
for element in array:
549
if predicate(element):
550
filtered.append(element)
551
return filtered
552
553
even_numbers = filter_array(numbers, lambda x: x % 2 == 0)
554
print(f"Even numbers: {list(even_numbers)}")
555
```
556
557
## Delta Operations
558
559
Array changes are represented as delta operations:
560
561
```python
562
# Example delta operations
563
delta_examples = [
564
{"retain": 2}, # Keep 2 elements
565
{"insert": ["a", "b"]}, # Insert elements
566
{"delete": 1}, # Delete 1 element
567
]
568
569
# Processing deltas
570
def apply_delta(array: Array, delta: list[dict]):
571
"""Apply a delta to array (conceptual example)."""
572
pos = 0
573
for op in delta:
574
if "retain" in op:
575
pos += op["retain"]
576
elif "insert" in op:
577
items = op["insert"]
578
for i, item in enumerate(items):
579
array.insert(pos + i, item)
580
pos += len(items)
581
elif "delete" in op:
582
for _ in range(op["delete"]):
583
del array[pos]
584
```
585
586
## Error Handling
587
588
```python
589
from pycrdt import Doc, Array
590
591
doc = Doc()
592
array = doc.get("items", type=Array)
593
594
try:
595
# Invalid index operations
596
array.insert(-1, "invalid") # May raise ValueError
597
598
# Out of bounds access
599
item = array[100] # May raise IndexError
600
601
# Invalid move operations
602
array.move(0, 100) # May raise ValueError
603
604
# Pop from empty array
605
array.clear()
606
array.pop() # May raise IndexError
607
608
except (ValueError, IndexError, TypeError) as e:
609
print(f"Array operation failed: {e}")
610
```