Metadata-Version: 2.4
Name: iris-global-reference
Version: 0.1.3
Summary: InterSystems IRIS Global Python API
Author-email: grongier <guillaume.rongier@intersystems.com>
Project-URL: homepage, https://github.com/grongierisc/iris-global-reference
Project-URL: documentation, https://github.com/grongierisc/iris-global-reference/blob/master/README.md
Project-URL: repository, https://github.com/grongierisc/iris-global-reference
Project-URL: issues, https://github.com/grongierisc/iris-global-reference/issues
Keywords: iris,intersystems,python,embedded
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Utilities
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: iris-embedded-python-wrapper>=0.5.0
Dynamic: license-file

# 1. IRIS Global Reference Python API

- Magic in place of Basic
- Some picture of globals (tree array)

A Pythonic wrapper around InterSystems IRIS globals providing an intuitive interface for working with hierarchical data.

- [1. IRIS Global Reference Python API](#1-iris-global-reference-python-api)
- [2. Introduction](#2-introduction)
- [3. Installation](#3-installation)
- [4. Basic Usage](#4-basic-usage)
- [5. Support of remote connection](#5-support-of-remote-connection)
- [6. Core Methods](#6-core-methods)
  - [6.1. Data Operations](#61-data-operations)
    - [6.1.1. set() Method](#611-set-method)
    - [6.1.2. get() Method](#612-get-method)
    - [6.1.3. kill() Method](#613-kill-method)
    - [6.1.4. delete() Method](#614-delete-method)
  - [6.2. Node Information](#62-node-information)
    - [6.2.1. data() Method](#621-data-method)
    - [6.2.2. has\_value() Method](#622-has_value-method)
    - [6.2.3. has\_descendants() Method](#623-has_descendants-method)
  - [6.3. Navigation](#63-navigation)
    - [6.3.1. next() Method](#631-next-method)
    - [6.3.2. query() Method](#632-query-method)
    - [6.3.3. order() Method](#633-order-method)
  - [6.4. Iteration](#64-iteration)
    - [6.4.1. subscripts() Method](#641-subscripts-method)
    - [6.4.2. keys() Method](#642-keys-method)
    - [6.4.3. values() Method](#643-values-method)
    - [6.4.4. items() Method](#644-items-method)
    - [6.4.5. zw() Method](#645-zw-method)
- [7. Dictionary \& JSON Operations](#7-dictionary--json-operations)
  - [7.1. to\_dict() Method](#71-to_dict-method)
  - [7.2. from\_dict() Method](#72-from_dict-method)
  - [7.3. to\_json() Method](#73-to_json-method)
  - [7.4. from\_json() Method](#74-from_json-method)
- [8. Python Magic Methods](#8-python-magic-methods)
- [9. Array support](#9-array-support)
- [10. Transaction support](#10-transaction-support)
- [11. Benefits](#11-benefits)
  - [11.1. Merge operation](#111-merge-operation)
- [12. Roadmap](#12-roadmap)
- [13. Compare to other libraries](#13-compare-to-other-libraries)
- [14. Testing](#14-testing)
- [15. License](#15-license)

# 2. Introduction

InterSystems IRIS is a powerful database platform that provides a unique hierarchical data structure called globals. Globals are a key-value store that can be used to store hierarchical data in a tree-like structure. This hierarchical data structure is very powerful but can be difficult to work with using traditional programming languages.

The IRIS Global Reference Python API is a Pythonic wrapper around InterSystems IRIS globals that provides an intuitive interface for working with hierarchical data. The API provides a set of core methods for working with globals, including data operations, node information, navigation, and iteration. The API also provides support for converting globals to dictionaries and JSON and back, making it easy to work with hierarchical data in Python.

Key differences with globals and dictionaries:

- Globals don't have a notion of arrays
  - This project provides a serialization to store arrays in globals
- A global node can have both values and child nodes
  - In a dictionary, a node can have either a value or child nodes, but not both
    - To solve this, the API binds the root node to `None` key
- Globals are ordered
  - Dictionaries are unordered

# 3. Installation

```bash
pip install git+https://github.com/grongierisc/iris-global-reference
```

to have direct access to an iris terminal, you can use the following command:

```bash
pip install git+https://github.com/grongierisc/iris-global-reference --target=<mgr_dir>/python
```

# 4. Basic Usage

Here's a comprehensive example showing various ways to interact with globals:

```python
from iris_global import GlobalReference

# Create a reference to a global
team = GlobalReference("^demo")
team.kill()  # Clear any existing data

# Different ways to set values
team.set((), "Baseball")                    # Set root node
team["name"] = "Boston Red Sox"             # Dictionary-style assignment
team.set(("players", "1"), "Babe Ruth")     # Tuple subscript
team.set(["players", "2"], "Cy Young")      # List subscript
team["players", "3"] = "Ted Williams"       # Multiple subscripts

# Different ways to get values
print(team.get(()))                         # Get root value: "Baseball"
print(team["name"])                         # Dictionary-style access
print(team.get(("players", "1")))          # Using get() method
print(team["players"]["2"])                # Nested dictionary-style

# Check if nodes exist
print(("players", "1") in team)            # True
print("nonexistent" in team)               # False

# Delete nodes
del team["players", "3"]                   # Delete using del
team.kill(("players", "2"))               # Delete using kill()

# Iteration Examples
# 1. Iterate through all nodes and values
for key, value in team.items():
    print(f"Node {key}: {value}")          # Shows all nodes with values

# Output :
# Node (): Baseball
# Node ('name',): Boston Red Sox
# Node ('players', '1'): Babe Ruth

# 2. Iterate through direct children only
for key in team.keys(children_only=True):
    print(f"Direct child: {key}")          # Shows only root level nodes

# Output:
# Direct child: ()
# Direct child: ('name',)

# 3. Custom iteration with subscripts
for sub in team.subscripts(("players",), children_only=True):
    print(f"Player: {team.get(sub)}")      # Shows only players

# Output:
# Player: Babe Ruth

# Count direct children
print(len(team))                           # Number of root level nodes

# Output:
# 3

# Display global structure
print(team.zw())                           # Show ZWRITE format

# Output:
# ^demo="Baseball"
# ^demo("name")="Boston Red Sox"
# ^demo("players","1")="Babe Ruth"

# Display this global in a dictionary format
print(team.to_dict())

# Output:
# {
#   None: 'Baseball',
#  'name': 'Boston Red Sox',
#  'players': {
#       '1': 'Babe Ruth'
#       }
# }
```

# 5. Support of remote connection

You can use the `GlobalReference` class with embedded Python by omitting the connection,
or connect to a remote IRIS instance by passing the native `iris` connection returned by
`iris.connect(...)`.

```python
import iris
from iris_global import GlobalReference

# Embedded Python, running inside IRIS.
team = GlobalReference("^demo")

# Remote connection, running outside IRIS.
conn = iris.connect("localhost", 1972, "USER", "SuperUser", "SYS")
team = GlobalReference("^demo", connection=conn)

# Set values
team.set((), "Baseball")
team.set(("name",), "Boston Red Sox")
team.set(("players", "1"), "Babe Ruth")

# Get values
print(team.get(()))  # Baseball
print(team.get(("name",)))  # Boston Red Sox
print(team.get(("players", "1")))  # Babe Ruth
```

SQLAlchemy engines and `iris://...` connection strings are no longer supported by
`GlobalReference`. Use `iris.connect(host, port, namespace, username, password)` for
remote access.

# 6. Core Methods

## 6.1. Data Operations

- `set(subscript, value)`: Set a value at the specified node
- `get(subscript)`: Get the value at the specified node
- `kill(subscript)`: Delete a node and all its descendants
- `delete(subscript)`: Same as kill() but more Pythonic

### 6.1.1. set() Method

The `set()` method can be used to set values at any node in the global:

```python
team.set((), "Baseball")
```

can be used to set the root node, while:

```python
team.set(("name",), "Boston Red Sox")
```

sets a child node with subscript "name".

set can also be used with `str` or `list` subscript:

```python
team.set("", "Baseball")
team.set("name", "Boston Red Sox")
team.set(["players", "1"], "Babe Ruth")
```

You can also use dictionary type subscript:

```python
team["name"] = "Boston Red Sox"
team["players", "1"] = "Babe Ruth"
team["players"]["1"] = "Babe Ruth"
```

### 6.1.2. get() Method

The `get()` method can be used to retrieve values at any node in the global:

```python
team.get(())  # Returns "Baseball"
team.get(("name",))  # Returns "Boston Red Sox"
team.get(("players", "1"))  # Returns "Babe Ruth"
```

get can also be used with `str` or `list` subscript:

```python
team.get("")  # Returns "Baseball"
team.get("name")  # Returns "Boston Red Sox"
team.get(["players", "1"])  # Returns "Babe Ruth"
```

You can also use dictionary type subscript:

```python
print(team["name"])  # Returns "Boston Red Sox"
print(team["players", "1"])  # Returns "Babe Ruth"
print(team["players"]["1"])  # Returns "Babe Ruth"
```

### 6.1.3. kill() Method

The `kill()` method can be used to delete a node and all its descendants:

```python
team.kill(("players",))
```

kill can also be used with `str` or `list` subscript:

```python
team.kill("players")
team.kill(["players"])
```

You can also use dictionary type subscript:

```python
del team["players"]
```

### 6.1.4. delete() Method

Same as `kill()` but more Pythonic.

## 6.2. Node Information

- `data(subscript)`: Get node status (0=undefined, 1=value only, 10=descendants only, 11=both)
- `has_value(subscript)`: Check if node has a value
- `has_descendants(subscript)`: Check if node has child nodes

### 6.2.1. data() Method

The `data()` method can be used to get the status of a node:

```python
from iris_global import GlobalReference

data = {
    None: "root",
    "a": {1: "value a"}
}

team = GlobalReference("^data").from_dict(data)

print(team.data(()))  # 11 (node with descendants and value)
print(team.data(("a",)))  # 10 (node with descendants only)
print(team.data(("a", 1)))  # 1 (node with value only)
print(team.data(("b",)))  # 0 (undefined node)
```

Works with `str` or `list` subscript:

```python
print(team.data(""))  # 11
print(team.data("a"))  # 10
print(team.data(["a", 1]))  # 1
print(team.data("b"))  # 0
```

### 6.2.2. has_value() Method

The `has_value()` method can be used to check if a node has a value:

```python
from iris_global import GlobalReference

data = {
    None: "root",
    "a": {1: "value a"}
}

team = GlobalReference("^data").from_dict(data)

print(team.has_value(()))  # True
print(team.has_value(("a",)))  # False
print(team.has_value(("a", 1)))  # True
print(team.has_value(("b",)))  # False
```

Works with `str` or `list` subscript:

```python
print(team.has_value(""))  # True
print(team.has_value("a"))  # False
print(team.has_value(["a", 1]))  # True
print(team.has_value("b"))  # False
```

### 6.2.3. has_descendants() Method

The `has_descendants()` method can be used to check if a node has child nodes:

```python
from iris_global import GlobalReference

data = {
    None: "root",
    "a": {1: "value a"}
}

team = GlobalReference("^data").from_dict(data)

print(team.has_descendants(()))  # True
print(team.has_descendants(("a",)))  # True
print(team.has_descendants(("a", 1)))  # False
print(team.has_descendants(("b",)))  # False
```

Works with `str` or `list` subscript:

```python
print(team.has_descendants(""))  # True
print(team.has_descendants("a"))  # True
print(team.has_descendants(["a", 1]))  # False
print(team.has_descendants("b"))  # False
```

## 6.3. Navigation

- `next(subscript, direction)`: Get next subscript at same level (direction=1) or previous (direction=-1)
- `previous(subscript)`: Get previous subscript at the same level 
- `query(subscript, direction)`: Navigate through nodes like ObjectScript $QUERY
- `order(subscript, direction)`: Legacy through nodes like ObjectScript $ORDER

### 6.3.1. next() Method

The `next()` method can be used to get the next subscript:

```python
from iris_global import GlobalReference

# Create a reference to a global
team = GlobalReference("^demo")

# Set values
team.set((), "Baseball")
team.set(("name",), "Boston Red Sox")
team.set(("players", "1"), "Babe Ruth")
team.set(("players", "2"), "Cy Young")

# Query nodes
print(team.next(()))  # Returns ("name",)
print(team.next(("name",)))  # Returns ("players",)
print(team.next(("players",)))  # Returns ("players", "1")
print(team.next(("players", "1")))  # Returns ("players", "2")
print(team.next(("players", "2")))  # Returns None
```

works with `str` or `list` subscript:

```python
print(team.next(""))  # Returns ("name",)
print(team.next("name"))  # Returns ("players",)
print(team.next("players"))  # Returns ("players", "1")
print(team.next(["players", "1"]))  # Returns ("players", "2")
print(team.next(["players", "2"]))  # Returns None
```

### 6.3.2. query() Method

Same as `next()`.

The `query()` method can be used to navigate through nodes like ObjectScript $QUERY:

```python
from iris_global import GlobalReference

# Create a reference to a global
team = GlobalReference("^demo")

# Set values
team.set((), "Baseball")
team.set(("name",), "Boston Red Sox")
team.set(("players", "1"), "Babe Ruth")
team.set(("players", "2"), "Cy Young")

# Query nodes
print(team.query(()))  # Returns ("name",)
print(team.query(("name",)))  # Returns ("players",)
print(team.query(("players",)))  # Returns ("players", "1")
print(team.query(("players", "1")))  # Returns ("players", "2")
print(team.query(("players", "2")))  # Returns None
```

works with `str` or `list` subscript:

```python
print(team.query(""))  # Returns ("name",)
print(team.query("name"))  # Returns ("players",)
print(team.query("players"))  # Returns ("players", "1")
print(team.query(["players", "1"]))  # Returns ("players", "2")
print(team.query(["players", "2"]))  # Returns None
```

### 6.3.3. order() Method

Legacy method to navigate through nodes like ObjectScript $ORDER:

```python
from iris_global import GlobalReference

# Create a reference to a global
team = GlobalReference("^demo")

# Set values
team.set((), "Baseball")
team.set(("name",), "Boston Red Sox")
team.set(("players", "1"), "Babe Ruth")
team.set(("players", "2"), "Cy Young")

# Get next subscript
print(team.order(()))  # Returns "name"
print(team.order(("name",)))  # Returns "players"
print(team.order(("players",)))  # Returns None
print(team.order(("players", "")))  # Returns "1"
print(team.order(("players", "1")))  # Returns "2"
print(team.order(("players", "2")))  # Returns None
```

works with `str` or `list` subscript:

```python
print(team.order(""))  # Returns "name"
print(team.order("name"))  # Returns "players"
print(team.order("players"))  # Returns None
print(team.order(["players", ""]))  # Returns "1"
print(team.order(["players", "1"]))  # Returns "2"
print(team.order(["players", "2"]))  # Returns None
```

## 6.4. Iteration

> Note: subscript value are always returned as string tuples

- `subscripts(subscript, direction, with_descendants, with_values, with_root, children_only)`: Get all subscripts at subscript level
- `keys(subscript, direction, with_descendants, with_values, with_root, children_only)`: Get all subscripts at subscript level
- `values(subscript, direction, with_descendants, with_values, with_root, children_only)`: Get all values at subscript level
- `items(subscript, direction, with_descendants, with_values, with_root, children_only)`: Get all subscripts and values at subscript level

### 6.4.1. subscripts() Method

The `subscripts()` method can be used to get all subscripts at a subscript level.

Signature:

- `subscripts(subscript, direction=1, with_descendants=False, with_values=False, with_root=False, children_only=False)`
  - `subscript`: Subscript to start from
  - `direction`: Direction (1=forward, -1=backward)
  - `with_descendants`: Include child nodes default: False
  - `with_values`: Only include nodes with values default: False
  - `with_root`: Include the start node default: False
  - `children_only`: Only include direct children default: False

> Note: subscripts aim to have the same behavior as ObjectScript $ORDER

```python
from iris_global import GlobalReference

# Create a reference to a global
team = GlobalReference("^demo")

# Set values
team.set((), "Baseball")
team.set(("name",), "Boston Red Sox")
team.set(("players", "1"), "Babe Ruth")
team.set(("players", "2"), "Cy Young")
team.set(("world_series", "1"), "1903")
team.set(("world_series", "2"), "1912")

# Get all subscripts
for subscript in team.subscripts():
    print(subscript) # Returns ('name',), ('players',), ('world_series',)

# Get subscripts starting from "players"
for subscript in team.subscripts(("players",)):
    print(subscript) # Returns ('world_series',)

# Get subscripts starting from "players" that are nested
for subscript in team.subscripts(("players","")):
    print(subscript) # Returns ('players', '1'), ('players', '2')
# or
for subscript in team.subscripts(("players",), children_only=True):
    print(subscript) # Returns ('players', '1'), ('players', '2')

# Get subscripts only with values
for subscript in team.subscripts(with_values=True):
    print(subscript) # Returns ('name',)

# Get subscripts only with descendants
for subscript in team.subscripts(with_descendants=True):
    print(subscript) # Returns ('name',), ('players',), ('players', '1'), ('players', '2'), ('world_series',), ('world_series', '1'), ('world_series', '2')

# Get subscripts with root node
for subscript in team.subscripts(with_root=True):
    print(subscript) # Returns (), ('name',), ('players',), ('world_series',)

# Get subscripts only with descendants and values
for subscript in team.subscripts(with_descendants=True, with_values=True):
    print(subscript) # Returns ('name',), ('players', '1'), ('players', '2'), ('world_series', '1'), ('world_series', '2')

# Get subscripts with root node and descendants and values
for subscript in team.subscripts(with_root=True, with_descendants=True, with_values=True):
    print(subscript) # Returns (), ('name',), ('players', '1'), ('players', '2'), ('world_series', '1'), ('world_series', '2')
```

### 6.4.2. keys() Method

The `keys()` method can be used to get all subscripts at a subscript level.

Signature:

- `keys(subscript, direction=1, with_descendants=True, with_values=True, with_root=True, children_only=False)`
  - `subscript`: Subscript to start from
  - `direction`: Direction (1=forward, -1=backward)
  - `with_descendants`: Include child nodes default: True
  - `with_values`: Only include nodes with values default: True
  - `with_root`: Include the start node default: True
  - `children_only`: Only include direct children default: False

> Note: keys aim to have the same behavior as Python dictionary keys()
> It means return all subscripts with values by descending all branches including the root node
> Basically, same as subscripts(with_descendants=True, with_values=True, with_root=True)
> Aim to be close as possible to ObjectScript ZWRITE

```python
from iris_global import GlobalReference

# Create a reference to a global
team = GlobalReference("^demo")

# Set values
team.set((), "Baseball")
team.set(("name",), "Boston Red Sox")
team.set(("players", "1"), "Babe Ruth")
team.set(("players", "2"), "Cy Young")
team.set(("world_series", "1"), "1903")
team.set(("world_series", "2"), "1912")

# Get keys
for key in team.keys():
    print(key) # Returns (), ('name',), ('players', '1'), ('players', '2'), ('world_series', '1'), ('world_series', '2')

# Get all keys
for key in team.keys(with_values=False): # Returns all subscripts even without values
    print(key) # Returns ('name',), ('players',), ('players', '1'), ('players', '2'), ('world_series',), ('world_series', '1'), ('world_series', '2')

# Get keys starting from "players"
for key in team.keys(("players",)):
    print(key) # Returns ('players', '1'), ('players', '2'), ('world_series', '1'), ('world_series', '2')

# Get keys starting from "players" that are nested
for key in team.keys(("players",), children_only=True):
    print(key) # Returns ('players', '1'), ('players', '2')
# or
for key in team.keys(("players","")):
    print(key) # Returns ('players', '1'), ('players', '2')
```

### 6.4.3. values() Method

The `values()` method can be used to get all values at a subscript level.

Signature:
- `values(subscript, direction=1, with_descendants=True, with_values=True, with_root=True, children_only=False)`
  - `subscript`: Subscript to start from
  - `direction`: Direction (1=forward, -1=backward)
  - `with_descendants`: Include child nodes default: True
  - `with_values`: Only include nodes with values default: True
  - `with_root`: Include the start node default: True
  - `children_only`: Only include direct children default: False

> Note: values aim to have the same behavior as Python dictionary values()
> It means return all values by descending all branches including the root node

```python
from iris_global import GlobalReference

# Create a reference to a global
team = GlobalReference("^demo")

# Set values
team.set((), "Baseball")
team.set(("name",), "Boston Red Sox")
team.set(("players", "1"), "Babe Ruth")
team.set(("players", "2"), "Cy Young")
team.set(("world_series", "1"), "1903")
team.set(("world_series", "2"), "1912")

# Get values
for value in team.values():
    print(value) # Returns "Baseball", "Boston Red Sox", "Babe Ruth", "Cy Young", "1903", "1912"
```

### 6.4.4. items() Method

The `items()` method can be used to get all subscripts and values at a subscript level.

Signature:
- `items(subscript, direction=1, with_descendants=True, with_values=True, with_root=True, children_only=False)`
  - `subscript`: Subscript to start from
  - `direction`: Direction (1=forward, -1=backward)
  - `with_descendants`: Include child nodes default: True
  - `with_values`: Only include nodes with values default: True
  - `with_root`: Include the start node default: True
  - `children_only`: Only include direct children default: False

> Note: items aim to have the same behavior as Python dictionary items()
> It means return all subscripts and values by descending all branches including the root node

```python
from iris_global import GlobalReference

# Create a reference to a global
team = GlobalReference("^demo")

# Set values
team.set((), "Baseball")
team.set(("name",), "Boston Red Sox")
team.set(("players", "1"), "Babe Ruth")
team.set(("players", "2"), "Cy Young")
team.set(("world_series", "1"), "1903")
team.set(("world_series", "2"), "1912")

# Get items
for key, value in team.items():
    print(key, value) # Returns (), "Baseball", ('name',), "Boston Red Sox", ('players', '1'), "Babe Ruth", ('players', '2'), "Cy Young", ('world_series', '1'), "1903", ('world_series', '2'), "1912"
```

### 6.4.5. zw() Method

The `zw()` method can be used to display the global in ObjectScript ZWRITE format:

```python
from iris_global import GlobalReference

# Create a reference to a global
team = GlobalReference("^demo")

# Set values
team.set((), "Baseball")
team.set(("name",), "Boston Red Sox")
team.set(("players", "1"), "Babe Ruth")
team.set(("players", "2"), "Cy Young")
team.set(("world_series", "1"), "1903")
team.set(("world_series", "2"), "1912")

print(team.zw())
# ^demo=Baseball
# ^demo("name")="Boston Red Sox"
# ^demo("players","1")="Babe Ruth"
# ^demo("players","2")="Cy Young"
# ^demo("world_series","1")="1903"
# ^demo("world_series","2")="1912"
```

# 7. Dictionary & JSON Operations

Convert globals to dictionaries or JSON and back.

- `to_dict(subscript, with_descendants, with_values, with_root, children_only, merge_leafs)`: Export global to dictionary
- `from_dict(data)`: Import dictionary to global
- `to_json(subscript, with_descendants, with_values, with_root, children_only, merge_leafs)`: Export global to JSON
- `from_json(json_str, root_name)`: Import JSON to global

## 7.1. to_dict() Method

The `to_dict()` method can be used to export a global to a dictionary.

Signature:
- `to_dict(subscript=None, with_descendants=True, with_values=True, with_root=True, children_only=False, merge_leafs=True)`
  - `subscript`: Subscript to start from
  - `with_descendants`: Include child nodes default: True
  - `with_values`: Only include nodes with values default: True
  - `with_root`: Include the start node default: True
  - `children_only`: Only include direct children default: False
  - `merge_leafs`: Merge leaf nodes into a single value default: True

```python
from iris_global import GlobalReference

# Create a reference to a global
team = GlobalReference("^demo")

# Set values
team.set((), "Baseball")
team.set(("name",), "Boston Red Sox")
team.set(("players", "1"), "Babe Ruth")
team.set(("players", "2"), "Cy Young")
team.set(("world_series", "1"), "1903")
team.set(("world_series", "2"), "1912")

# Export to dictionary
data_dict = team.to_dict()
print(data_dict)

# returns:
# {
#     None: "Baseball",
#     "name": "Boston Red Sox",
#     "players": {
#         "1": "Babe Ruth",
#         "2": "Cy Young"
#     },
#     "world_series": {
#         "1": "1903",
#         "2": "1912"
#     }
# }
# Note: root node is bound to None key
# Note: leaf nodes are merged by default

# Export to dictionary unmeged
print(team.to_dict(merge_leafs=False))
# returns: 
# {
#     None: "Baseball",
#     "name": {None: "Boston Red Sox"},
#     "players": {
#         "1": {None: "Babe Ruth"},
#         "2": {None: "Cy Young"}
#     },
#     "world_series": {
#         "1": {None: "1903"},
#         "2": {None: "1912"}
#     }
# }
```

## 7.2. from_dict() Method

The `from_dict()` method can be used to import a dictionary to a global.

```python
from iris_global import GlobalReference

# Create a reference to a global
team = GlobalReference("^demo")

# Dictionary data
data_dict = {
    None: "Baseball",
    "name": "Boston Red Sox",
    "players": {
        "1": "Babe Ruth",
        "2": "Cy Young"
    },
    "world_series": {
        "1": "1903",
        "2": "1912"
    }
}

# Note: root node is bound to None key

# Import dictionary
team.from_dict(data_dict)

# Unmerged dictionary data can be imported as well
data_dict = {
    None: "Baseball",
    "name": {None: "Boston Red Sox"},
    "players": {
        "1": {None: "Babe Ruth"},
        "2": {None: "Cy Young"}
    },
    "world_series": {
        "1": {None: "1903"},
        "2": {None: "1912"}
    }
}

team_unmerged = GlobalReference("^unmerged")
team_unmerged.from_dict(data_dict)

# Check values
team_unmerged.to_dict() == team.to_dict()  # True
```

## 7.3. to_json() Method

The `to_json()` method can be used to export a global to JSON.

Signature:
- `to_json(subscript=None, with_descendants=True, with_values=True, with_root=True, children_only=False, merge_leafs=True, root_name="_")`
  - `subscript`: Subscript to start from
  - `with_descendants`: Include child nodes default: True
  - `with_values`: Only include nodes with values default: True
  - `with_root`: Include the start node default: True
  - `children_only`: Only include direct children default: False
  - `merge_leafs`: Merge leaf nodes into a single value default: True
  - `root_name`: Rename the root node

```python
from iris_global import GlobalReference

# Create a reference to a global
team = GlobalReference("^demo")

# Set values
team.set((), "Baseball")
team.set(("name",), "Boston Red Sox")
team.set(("players", "1"), "Babe Ruth")
team.set(("players", "2"), "Cy Young")
team.set(("world_series", "1"), "1903")
team.set(("world_series", "2"), "1912")

# Export to JSON
print(team.to_json())
# returns: 
# {
#     "name": "Boston Red Sox",
#     "players": {
#         "1": "Babe Ruth",
#         "2": "Cy Young"
#     },
#     "world_series": {
#         "1": "1903",
#         "2": "1912"
#     },
#     "_": "Baseball"
# }
# Note: root node is bound to "_"
# Note: leaf nodes are merged by default

# Export to JSON unmeged
print(team.to_json(merge_leafs=False))
# returns:
# {
#     "name": {"_": "Boston Red Sox"},
#     "players": {
#         "1": {"_": "Babe Ruth"},
#         "2": {"_": "Cy Young"}
#     },
#     "world_series": {
#         "1": {"_": "1903"},
#         "2": {"_": "1912"}
#     },
#     "_": "Baseball"
# }

# Root node can be renamed
print(team.to_json(root_name="__root__"))
# returns:
# {
#     "name": "Boston Red Sox",
#     "players": {
#         "1": "Babe Ruth",
#         "2": "Cy Young"
#     },
#     "world_series": {
#         "1": "1903",
#         "2": "1912"
#     },
#     "__root__": "Baseball"
# }
```

## 7.4. from_json() Method

The `from_json()` method can be used to import JSON to a global.

```python
import json
from iris_global import GlobalReference

# Create a reference to a global
team = GlobalReference("^demo")

# JSON data
json_str = """
{
    "name": "Boston Red Sox",
    "players": {
        "1": "Babe Ruth",
        "2": "Cy Young"
    },
    "world_series": {
        "1": "1903",
        "2": "1912"
    },
    "_": "Baseball"
}
"""

# Import JSON
team.from_json(json_str)

# Check values
print(json.loads(json_str) == json.loads(team.to_json()))  # True

# Root node can be renamed
json_str = """
{
    "name": "Boston Red Sox",
    "players": {
        "1": "Babe Ruth",
        "2": "Cy Young"
    },
    "world_series": {
        "1": "1903",
        "2": "1912"
    },
    "__root__": "Baseball"
}
"""

team.from_json(json_str, root_name="__root__")

# Check values
print(json.loads(json_str) == json.loads(team.to_json(root_name="__root__")))  # True
```

# 8. Python Magic Methods

GlobalReference implements Python magic methods for a more Pythonic experience:

```python
# Dictionary-like access
team["name"] = "Boston Red Sox"
print(team["name"])
del team["players", "3"]

# Check if node exists
if ("players", "1") in team:
    print("Player exists!")

# Get number of direct child nodes
num_nodes = len(team)

# Iterate directly
for key,value in team:
    print(key, value)
```

Values returned with `[]` are `Key` objects. For string, integer, float, and bytes values,
the returned key is also a subclass of the matching Python primitive:

```python
name = team["name"]
isinstance(name, str)  # True
```

Bracket access on a `Key` is always global child access, not native primitive indexing.
Use `.value`, `str(key)`, or `bytes(key)` when you need primitive indexing:

```python
team["name"][0]        # Looks up subscript ("name", 0)
team["name"].value[0]  # Returns first character from the string value
```

Boolean values are returned as plain `Key` wrappers because Python does not allow
subclassing `bool`.

# 9. Array support

> Note: Array support is experimental

To support array from dict/json, i used a serialization to store the array in a global.
The serialization is the following:

- the root node contains an **serialization string** eg: `__array__`
- the children nodes contains the array values
- the children nodes are named with the **serialization string** and the index of the array

example:

```python
from iris_global import GlobalReference

GlobalReference("demo").kill()

gref = GlobalReference("demo")

my_dict = {"a": 1, "b": 2, "array": [1, 2, 3], "array_of_dicts": [{"a": 1}, {"b": 2}]}
gref.from_dict(my_dict)

print(gref.to_dict()) # {'a': 1, 'array': [1, 2, 3], 'array_of_dicts': [{'a': 1}, {'b': 2}], 'b': 2}
print(gref.zw())
# ^demo("a")=1
# ^demo("array")="__array__"
# ^demo("array","__array__0")=1
# ^demo("array","__array__1")=2
# ^demo("array","__array__2")=3
# ^demo("array_of_dicts")="__array__"
# ^demo("array_of_dicts","__array__0","a")=1
# ^demo("array_of_dicts","__array__1","b")=2
# ^demo("b")=2

# can still present global as dict without serialization
print(gref.to_dict(merge_array=False)) 
# returns:
# {
#     'a': 1,
#     'array': {None: '__array__', '__array__0': 1, '__array__1': 2, '__array__2': 3},
#     'array_of_dicts': {None: '__array__', '__array__0': {'a': 1}, '__array__1': {'b': 2}},
#     'b': 2
# }

# the serialization string can be changed
gref.kill()
gref.from_dict(my_dict, array_prefix="__list__")
print(gref.zw())
# ^demo("a")=1
# ^demo("array")="__list__"
# ^demo("array","__list__0")=1
# ^demo("array","__list__1")=2
# ^demo("array","__list__2")=3
# ^demo("array_of_dicts")="__list__"
# ^demo("array_of_dicts","__list__0","a")=1
# ^demo("array_of_dicts","__list__1","b")=2
# ^demo("b")=2
```

# 10. Transaction support

Transactions can be supported by using the `with` statement:

```python
from iris_global import GlobalReference

# Create a reference to a global with a transaction
with GlobalReference("^demo") as team:
    team.set((), "Baseball")
    team.set(("name",), "Boston Red Sox")
    team.set(("players", "1"), "Babe Ruth")
    team.set(("players", "2"), "Cy Young")
```

or

```python
from iris_global import GlobalReference

# Create a reference to a global
team = GlobalReference("^demo")

# Set values
team.set((), "Baseball")

# Start transaction
with team:
    team.set(("name",), "Boston Red Sox")
    team.set(("players", "1"), "Babe Ruth")
    team.set(("players", "2"), "Cy Young")
```

Otherwize, you can use the `begin()`, `commit()` and `rollback()` methods:

```python
from iris_global import GlobalReference

# Create a reference to a global
team = GlobalReference("^demo")

# Set values
team.set((), "Baseball")

# Start transaction
team.begin()
team.set(("name",), "Boston Red Sox")

# Commit transaction
team.commit()
```


# 11. Benefits

- **Pythonic**: Provides a Pythonic interface for working with globals
- **Intuitive**: Simplifies working with hierarchical data
- **Efficient**: Reduces the need for complex ObjectScript code
- **Flexible**: Supports a wide range of data operations

## 11.1. Merge operation

ObjectScript has a merge operation that can be used to merge two globals. This operation is not supported by the API. However, you can achieve the same result by exporting the globals to dictionaries, merging the dictionaries, and then importing the merged dictionary back to a global.

```python
from iris_global import GlobalReference

# Create a reference to a global
team1 = GlobalReference("^team1")
team11 = GlobalReference("^team11")
team2 = GlobalReference("^team2")

# Set values
team1.set((), "Baseball")
team1.set(("name",), "Boston Red Sox")
team1.set(("players", "1"), "Babe Ruth")

team11.set((), "Baseball")
team11.set(("name",), "Boston Red Sox")
team11.set(("players", "1"), "Babe Ruth")

team2.set((), "Baseball")
team2.set(("name",), "New York Yankees")
team2.set(("players", "2"), "Lou Gehrig")

# Export to dictionaries
data1 = team1.to_dict()
data2 = team2.to_dict()

# Merge dictionaries
data1.update(data2)

# Import merged dictionary
team1.from_dict(data1)
prin(team1.zw())
# ^team1=Baseball
# ^team1("name")=New York Yankees
# ^team1("players","1")=Babe Ruth
# ^team1("players","2")=Lou Gehrig

# or
team11.from_dict(team2)
prin(team11.zw())
# ^team11=Baseball
# ^team11("name")=New York Yankees
# ^team11("players","1")=Babe Ruth
# ^team11("players","2")=Lou Gehrig
```

The benefit of this approach is that it allows you to use Python's built-in dictionary operations to manipulate globals, making it easier to work with hierarchical data structures.
For the more, no need to build a complex python code to merge two globals.

# 12. Roadmap

- [ ] Add support for global arrays
- [ ] Add support for byte set/get
- [ ] Add support for listbuild, vector, pva, bit
- [ ] Add support of mutli-dimensional variables

# 13. Compare to other libraries

This table compares the api of the iris_global library with other libraries, only low-level operations are compared:

| Feature | iris_global | irisnative | embedded-python | comments |
| --- | --- | --- | --- | --- |
| initialize | `global_reference = GlobalReference("^demo", connection=conn)` | `conn = iris.connect(...); global_reference = iris.createIRIS(conn)` | `global_reference = iris.gref("^demo")` | omit `connection` for embedded Python |
| set value | `global_reference.set(("name",1), "Boston Red Sox")` | `global_reference.set("Boston Red Sox", "demo", "name", 1)` | `global_reference.set(["name",1], "Boston Red Sox")` | for irisnative, the value is the first argument |
| get value | `global_reference.get(("name",1))` | `global_reference.get("demo", "name", 1)` | `global_reference.get(["name",1])` | |
| check if node exists | `global_reference.data(("name",1))` | `global_reference.isDefined("demo", "name", 1)` | `global_reference.data(["name",1])` | |
| delete node | `global_reference.kill(("name",1))` | `global_reference.kill("demo", "name", 1)` | `global_reference.kill(["name",1])` | |
| order | `global_reference.order(("name",1))` | `global_reference.nextSubscript(False, "demo", "name", 1)` | `global_reference.order(["name",1])` | for irisnative, the reverse flag is the first argument |

General remarks:
- iris_global support for get/set can be used with str, list or tuple subscript
- irisnative use the value as the first argument
- embedded-python use list subscript
- irisnative use nextSubscript instead of order
- irisnative use isDefined instead of data

# 14. Testing

Run the test suite:

```bash
python -m pytest
```

# 15. License

MIT License - see LICENSE for details.
