# Examples
## Data Structure Operations
Note: there are some operations that are available to all Containers, which are shown at the [end of this page](#all-containers).\
For all operations, a store and a database are needed:
```python
from gink import *
store = LmdbStore('example.db')
database = Database(store=store)
```
### Box
A Box is the simplest data structure available on Gink. It can hold only one value at a time; you can set its value, or get its value.
```python
box = Box(database=database)
box.set({"foo": "bar", "key2": 15})
result = box.get() # Returns the python dictionary just added
if not box.is_empty():
print(box.size()) # This will only return 0 or 1 (1 in this case).
```
### Directory
The Directory aims to mimic the functionality of a Python dictionary. If you know how to use a dictionary, you should already know how to use the directory!
```python
directory = Directory(database=database)
directory["key1"] = "value1"
# Saves a timestamp after "key1" and before "key2"
time = database.get_now()
# Achieves the same thing as the previous set, just different syntax.
directory.set("key2", {"test": "document"})
result = directory.get("key2") # Returns {"test": "document"}
result2 = directory.get("key3") # Returns None
# Returns an generator of ["key1", "key2"]
# Note: the order may not be the same.
keys = directory.keys()
# Returns the items as a generator of (key, value tuples) in the directory
# at the specified timestamp - in this case, [("key1", "value1")]
items = directory.items(as_of=time)
# Returns "value1" and removes the key: value pair from the directory.
value = directory.pop("key1")
```
### Sequence
The Sequence is equivalent to a Python list. Again, these operations should look pretty familiar. In a Gink Sequence, the contents are ordered by timestamps.
```python
sequence = Sequence()
sequence.append("Hello, World!")
sequence.append(42)
sequence.append("a")
sequence.append("b")
found = sequence.index("Hello, World!")
# Returns 0
popped = sequence.pop(1)
# Returns 42
# Pops and returns the value at index 0, which is "Hello, World!"
# The destination argument allows you to place the item
# back into the sequence at a different timestamp
# in this case, -1 would indicate the timestamp of the last change.
# So, this sequence is now ordered as ["a", "b", "Hello, World!"]
popped = sequence.pop(0, dest=-1)
# Inserts "x" at index 1, between "a" and "b", in this example.
# Comment is an optional parameter that will be included in
# bundle for this change (most operations may contain comments).
sequence.insert(1, "x", comment="insert x")
# Convert contents to Python list
as_list = list(sequence)
```
### Key Set
The Key Set is designed to work similarly to a Python Set - it is just a "list" of unique keys. If you are looking for a data structure to hold a reference to another container (or a Muid), check out [Group](#group-examples) below.\
Note: a Key Set can hold keys of types str, int, and bytes.
```python
ks = KeySet(database=database)
ks.add("key1")
ks.add("key2")
is_contained = ks.contains("key1") # Returns True
ks.remove("key1")
ks.update(["key3", "key4"]) # adds multiple keys
popped = ks.pop("key2") # returns "key2"
# Our Python KeySet also includes operations such as
# issubset, issuperset, etc. I encourage you to check out
# the full docs to see the other methods.
ks.update(["key1", "key2"])
# keyset is now ["key1", "key2", "key3", "key4"]
is_subset = ks.issubset(["key1", "key2", "key3", "key4", "key5"])
# returns True
union = ks.union(["key4", "key5"])
# returns ["key1", "key2", "key3", "key4", "key5"]
# however, the return value is not ordered.
```
### Pair Set
The Pair Set is the first data structure out of the previous examples that has few similarities to the build-in Python data structures. In Gink, every container is given a "Muid" upon creation ([Muid docs](#muid)). In simple terms, a Muid is just a unique identifier to keep track of containers (and changes).\
While the Pair Set's methods do not mimic those of the Python Set, you can think of a Pair Set as a set of tuples. These tuples contain pairs of (Muid, Muid), or (Vertex, Vertex) (more on Vertexs [here](#vertex-examples)). Basically, a Pair Set serves to store the fact that two Vertexs are connected.
```python
ps = PairSet()
vertex1 = Vertex()
vertex2 = Vertex()
# "Include" refers to the fact that the pair is either
# in the pair set, or it is not.
ps.include(pair=(vertex1, vertex2))
ps.exclude(pair=(vertex1, vertex2))
# Same as above, but adding the pair using muids.
ps.include(pair=(vertex1._muid, vertex2._muid))
is_contained = ps.contains(pair=(vertex1, vertex2)) # returns True
pairs = ps.get_pairs() # returns Set{(vertex1._muid, vertex2._muid)}
```
### Pair Map
Similar to the Pair Set, a Pair Map has keys consisting of a (Vertex, Vertex) or (Muid, Muid) tuple. These keys are mapped to a value, which may be a Container or other standard value (str, int, list, etc.). The Pair Map has methods similar to those found in the built in map object.
```python
pm = PairMap(database=database)
vertex1 = Vertex()
vertex2 = Vertex()
pm.set(key=(vertex1, vertex2), value="vertex1->vertex2")
in_pm = pm.has(key=(vertex1._muid, vertex2._muid)) # returns True
value = pm.get(key=(vertex1, vertex2)) # returns "vertex1->vertex2"
items = pm.items() # returns a generator of ((Muid, Muid), value)
```
(group-examples)=
### Group
A Group is simply a collection of Containers that have something in common.
```python
group = Group(database=database)
vertex1 = Vertex()
vertex2 = Vertex()
group.include(vertex1)
group.include(vertex2._muid)
# returns a generator of the member Muids
member_muids = group.get_member_ids()
# returns a Set of the member Containers
members = group.get_members()
group.exclude(vertex1)
```
### Property
A Property is used to tie a particular object to a value, which may be any standard value, or another Container.
```python
prop = Property(database=database)
directory = Directory(database=database)
prop.set(directory, "my favorite directory")
contents = prop.get(directory) # Returns "my favorite directory"
# This overwrites the previous property
prop.set(directory, {"key1": "value1", "key2": "value2"})
prop.delete(directory)
```
### Graph
(vertex-examples)=
#### Vertex
The Vertex is a core part of Gink's graph data structure. If you are familiar with graph databases, the Gink Vertex is comparable to a Node. The Vertex is designed to connect to other nodes through edges, which is described below.
```python
# Basic creation and deletion
user_vertex = Vertex(database=database)
order_vertex = Vertex(database=database)
user_vertex.remove()
order_vertex.remove()
is_alive = user_vertex.is_alive() # returns False since we removed it.
# Most of the Vertex functionality comes when using
# an edge - more examples below.
```
#### Edge and EdgeType
An Edge is what connects a Vertex to another Vertex. The EdgeType is the `action` of an Edge, or the relationship between the vertexs. For example, one vertex may be a user, while the other node is an order. This may be depicted as (User)--Ordered-->(Order). User and Order are Vertexs, Ordered is the EdgeType, and the lines connecting them (and the direction) is the Edge.
```python
user_vertex = Vertex(database=database)
order_vertex = Vertex(database=database)
# An easy way to connect vertexs is by creating a EdgeType
ordered_verb = EdgeType(database=database)
ordered_verb.create_edge(user_vertex, order_vertex, "Ordered")
# We can get all edges of any EdgeType (can specify source or target) since
# we only have one edge, we don't need to specify a source or target here.
edges = ordered_verb.get_edges()
# Above returns a generator of edges, so lets get the only edge we have so far
ordered_edge = list(edges)[0]
# Now we can get the source, target, and action of this edge
# The action, in this context, is the actual message of the edge_type, "Ordered"
source = ordered_edge.get_source()
target = ordered_edge.get_target()
action = ordered_edge.get_action()
# To remove the edge
ordered_edge.remove()
```
(all-containers)=
### All Containers
The Container is the parent class for all Gink data structures. Here are some examples of the powerful operations you can do with any container:
#### Global Instance
For each Container type there's a pre-existing global instance with address `Muid(timestamp=-1, medallion=-1, offset=behavior)`. This container type can be written to by any instance, and may be used to coordinate between database instances or just for testing/demo purposes.
```python
global_directory = Directory.get_global_instance(database=database)
global_box = Box.get_global_instance(database=database)
```
##### Recommendations
Below are our recommendations for how to use a few of the global containers: \
The global property is a special container that is used to set the names of containers. While you can still use it for other purposes, Gink assumes it will store container names. \
The global directory does not necessarily have a set use, but we recommend you use this to
store references to other containers you create that you may want to access from other databases, reaccess if your page refreshes, etc. \
The global box can be used in a similar manner as the global directory, but it will only hold one container.
#### From Contents
To make it easier to insert data into an object upon initialization, Gink allows you to specify a `contents` argument to the constructor of the object. Different data structures may take different types as contents, but the idea remains the same for all Gink objects.
```python
directory = Directory(database=database,
contents={
"key1": "value1",
"key2": 42,
"key3": [1, 2, 3, 4]
})
key_set = KeySet(database=database, contents=["key1", "key2", 3])
# Vertex creation for pair map population
vertex1 = Vertex()
vertex2 = Vertex()
# Pair Map contents only takes a dictionary. Read the docs for the
# accepted data types for other data structures.
pair_map = PairMap(contents={(vertex1, vertex2): "value"})
```
#### Back in time (as_of)
You will frequently see `as_of` in the Gink documentation. `as_of` refers to the time to look back to. There are multiple ways of interacting with `as_of`. If you are curious about how certain timestamps are resolved, take a look at `Database.resolve_timestamp()`\
One easy way is to pass a negative integer indicating how many changes back you want to look.
```python
box = Box(contents="first_value")
box.set("second_value")
# Passing -1 into the as_of argument looks back at the previous value
# Returns "first_value"
previous = box.get(as_of=-1)
```
Another common way to use timestamps is to "save" a time between changes as a variable.
```python
box = Box(contents="first_value")
time_after_first = database.get_now()
box.set("second_value")
# Passing saved timestamp into as_of
# Returns "first_value"
previous = box.get(as_of=time_after_first)
```
#### Reset
Resetting a container is a fundamental operation used to revert the container back to a previous time. Above we looked at using timestamps to get previous values, but resetting to specific times may prove more useful. This example uses a directory, but this functionality works the same for all containers.
```python
directory = Directory()
directory["foo"] = "bar"
directory["bar"] = "foo"
time_between = database.get_now()
directory[7] = {"user": 1003203, "email": "test@test.com"}
has_7 = 7 in directory # returns True
directory.reset(to_time=time_between)
has_7 = 7 in directory # now returns False
has_bar = "bar" in directory # still returns True
```
#### Clearing
Clearing a container does exactly what you would expect it to do. The `Container.clear()` method removes all entries from the container and returns the Muid of the clearance. The clearance is processed as a new database change, which means you can still look back at previous timestamps or reset the database back to before the clearance occurred.
```python
directory = Directory()
directory["foo"] = "bar"
directory["bar"] = "foo"
directory[7] = {"user": 1003203, "email": "test@test.com"}
# Storing the muid of the clearance to use later
clearance_muid = directory.clear()
# Directory is now empty
has_foo = "foo" in directory # Returns False
has_bar = "bar" in directory # Returns False
has_7 = "7" in directory # Returns False
# Using the muid's timestamp to look back before the clearance
# Returns "bar"
previous = directory.get("foo", as_of=clearance_muid.timestamp)
```
#### Dumps
The `Container.dumps()` method dumps the contents of a container into a string. This string can `eval` back into a Gink object, so this method can be used for backup purposes.
```python
# Dumps using PairSet
vertex1 = Vertex(database=database)
vertex2 = Vertex(database=database)
vertex3 = Vertex(database=database)
pairset1 = PairSet(contents=[
(vertex1, vertex2),
(vertex1, vertex3),
(vertex2, vertex3)
], database=database)
dump = pairset1.dumps()
pairset2 = eval(dump)
# Returns 3, since this is a new object with the same
# contents as the original
new_size = pairset2.size()
```
## Database Operations
#### Bundling, comments, and commits
A bundle is simply a collection of changes with an optional comment/message, like a commit in Git. Without specifying a bundler object, Gink operations will immediately bundle the change in its own bundle, so you don't have to worry about always creating a new bundler, etc. However, if you do want to specify which changes go into a specific bundle (and when to bundle them), here is an example:
```python
directory = Directory()
bundler = Bundler(comment="example setting values in directory")
directory.set("key1", 1, bundler=bundler)
directory.set("key2", "value2", bundler=bundler)
directory.update({"key3": 3, "key4": 4}, bundler=bundler)
# This seals the bundler and bundles changes to database
# at this point, no more changes may be added
database.bundle(bundler)
```
### Reset
Similar to how `Container.reset()` works, the Database class has its own reset functionality that will reset all containers to the specified time. A "reset" is simply one large bundle of changes that updates the database entries to what they were are the previous timestamp; this allows you to easily look back before the reset.
```python
database = Database(store=store)
root = Directory.get_global_instance(database=database)
queue = Sequence.get_global_instance(database=database)
misc = Directory()
misc["yes"] = False
root["foo"] = "bar"
queue.append("value1")
# No as_of argument defaults to EPOCH
# which is the time of database creation (empty)
database.reset()
# All containers will have a length of 0
# since the database is now empty.
size = len(root)
# to_time=-1 reverts the database to the
# previous change
database.reset(to_time=-1)
# This will now have a len of 1,
# and one element of "value1"
size = len(queue)
```