Metadata-Version: 2.4
Name: tango-keystore
Version: 0.5.0
Summary: Type-preserving key-value store for Tango database
Author-email: Ezequiel Panepucci <ezequiel.panepucci@maxiv.lu.se>
License-Expression: GPL-3.0-or-later
Project-URL: Homepage, https://gitlab.com/maxiv/tools/tango-keystore
Project-URL: Repository, https://gitlab.com/maxiv/tools/tango-keystore/
Project-URL: Issues, https://gitlab.com/maxiv/tools/tango-keystore/issues
Keywords: tango,database,keystore,controls,maxiv
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pytango>=9.3.0
Requires-Dist: rich>=13.0.0
Provides-Extra: tests
Requires-Dist: pytest; extra == "tests"
Requires-Dist: pytest-cov; extra == "tests"
Requires-Dist: pytango-db; extra == "tests"
Provides-Extra: dev
Requires-Dist: ipython; extra == "dev"
Dynamic: license-file

# Tango Free Properties with Type Preservation

A Python module for managing key-value pairs in Tango database's free property section while preserving Python types.

Properties go into the **TangoKeystore** namespace by default.

<img alt="Jive free properties" src="docs/jive-free-properties.png" width="400">

## Problem

Tango database stores all properties as strings. When you store a float like `1.2`, it gets stored as the string `'1.2'`. This module solves this by storing type metadata alongside the value, enabling proper type restoration upon retrieval.

## Features

- ✅ Preserves original Python types (int, float, str, bool, list, dict, None)
- ✅ Automatic serialization/deserialization using JSON
- ✅ Simple, intuitive API
- ✅ Bulk operations (get_all, clear)
- ✅ Key existence checking
- ✅ No default values, keys must exist before being accessed
- ✅ Keys must be explicitly created before being accessed
  - either with `must_exist=False` or by using the `create` method.
- ✅ Key name validation: `r"^[a-zA-Z_][a-zA-Z0-9_]*$"`
  - this means keys are also valid python identifiers

The command line tool takes input in the **JSON format** whereas the python module uses **Python types**.

## Value Type Parsing

The CLI automatically detects and converts value types:

| Input | Parsed Type | Example |
|-------|-------------|---------|
| `42` | int | `count=42` |
| `-10` | int | `offset=-10` |
| `3.14` | float | `pi=3.14` |
| `-2.5` | float | `delta=-2.5` |
| `true`, `false` | bool | `enabled=true` |
| `none`, `null` | NoneType | `optional=none` |
| `[1,2,3]` | list | `values=[1,2,3]` |
| `{"a":1}` | dict | `config={"timeout":30}` |
| `hello` | str | `name=hello` |


## Installation

`tango-keystore` is on [PyPI](https://pypi.org/project/tango-keystore/) and [conda-forge](https://github.com/conda-forge/tango-keystore-feedstock).
Install it with your preferred package manager: `pip`, `uv`, `conda` or `pixi`.

```bash
pip install tango-keystore
```

## Usage


### Basic Example

```python
from tango_keystore import TangoKeystore

# Initialize
keystore = TangoKeystore()

# Store values with automatic type preservation
keystore.put("omega_reference", 1.2, must_exist=False)
keystore.put("iteration_count", 42, must_exist=False)
keystore.put("is_enabled", True, must_exist=False)

# Retrieve with correct types
omega = keystore.get("omega_reference")  # Returns float 1.2
count = keystore.get("iteration_count")  # Returns int 42
enabled = keystore.get("is_enabled")    # Returns bool True

print(type(omega))  # <class 'float'>
```

### Complex Types

```python
# Store lists and dictionaries
keystore.put("coordinates", [10.5, 20.3, 30.1])
keystore.put("config", {"timeout": 30, "retry": 3})

# Retrieve with proper types
coords = keystore.get("coordinates")  # Returns list
config = keystore.get("config")       # Returns dict
```

### utilities
```python
# get history for a Key (timestamp is epoch)
history = keystore.get_history("omega_reference")
[{'key': 'omega_reference',
  'value': 1.2,
  'type': 'float',
  'timestamp': 1763123518.0},
 {'key': 'omega_reference',
  'value': 4.2,
  'type': 'float',
  'timestamp': 1763123535.0},
 {'key': 'omega_reference',
  'value': 4.2,
  'type': 'float',
  'timestamp': 1763123740.0}]

# feature flags
keystore.put("room_temperature", "on", must_exist=False)
keystore.is_active("room_temperature")
True
keystore.is_on("room_temperature")
True
keystore.is_active("room_temperature")
True
keystore.is_inactive("room_temperature")
False
keystore.is_off("room_temperature")
False
```

### Additional Operations

```python
# Check if key exists
if keystore.exists("omega_reference"):
    print("Key exists!")

# get with metadata
keystore.get_with_metadata("temperature")
{'value': 35.0, 'type': 'float'}

# Get all keys
all_keys = keystore.get_all_key_names()

# Get all properties as dictionary
all_props = keystore.get_all()

# Delete a property
keystore.delete("omega_reference")

# Clear all properties, requires confirmation code.
# Freja+ coming soon.
keystore.clear()
Type confirmation code to continue => 18398  : 18398
```

### Using Custom Database Connection

```python
import tango

# Use specific database
db = tango.Database("host", "port")
keystore = TangoKeystore(db=db)
```

## API Reference

### `TangoKeystore(namespace="FunkyKeystore)`
Initialize the manager with optional namespace.

### `put(key, value)`
Store a value with type preservation.
- **key**: Property name (str)
- **value**: Value to store (must be JSON-serializable)

### `get(key)`
Retrieve a value with type restoration.
- **key**: Property name (str)
- **Returns**: Original value with proper type

### `delete(key)`
Delete a property.

### `exists(key)`
Check if a property exists.
- **Returns**: bool

### `get_all_keys()`
Get all property keys.
- **Returns**: list of keys

### `get_all()`
Get all properties as a dictionary.
- **Returns**: dict mapping keys to typed values

### `clear()`
Delete all properties (use with caution!).

## How It Works

The module wraps each value in a JSON object containing both the value and its type:

```python
# When you call: keystore.put("omega_reference", 1.2)
# It stores: '{"value": 1.2, "type": "float"}'

# When you call: keystore.get("omega_reference")
# It retrieves the JSON, extracts the value and type, and returns: 1.2 (as float)
```

## Supported Types

- `int`
- `float`
- `str`
- `bool`
- `list`
- `dict`
- `NoneType` (None)

## Error Handling

- Raises `TypeError` for unsupported types
- Raises `ValueError` for corrupted or invalid data
- Returns default value if key doesn't exist (instead of raising exception)

## License

See `LICENSE` file.

## Disclaimer

This package was initially produced with AI tools from Claude by Anthropic.
