# types - Common data types

## Overview

## `firebird.base.types`

This module provides fundamental building blocks used across the `firebird-base` package and potentially other Firebird Python projects. It includes:

- A custom base exception class (Error).
- Utilities for creating Singletons (Singleton).
- A robust implementation for Sentinel objects (Sentinel) and common predefined sentinels.
- Base classes for objects with distinct identities based on keys (Distinct, CachedDistinct).
- Enumerations for specific concepts (ByteOrder, ZMQTransport, ZMQDomain).
- Enhanced string types with validation and added functionality (ZMQAddress, MIME, PyExpr, PyCode, PyCallable).
- Metaclass utilities (conjunctive).
- Helper functions (load).

## Exceptions

## `firebird.base.types.Error`

Bases: `Exception`

Exception intended as a base for application-related errors.

Unlike the standard Exception, this class accepts arbitrary keyword arguments during initialization. These keyword arguments are stored as attributes on the exception instance.

Attribute lookup on instances of Error (or its subclasses) will return `None` for any attribute that was not explicitly set via keyword arguments during `__init__`, preventing AttributeError for common checks.

> [!IMPORTANT]
>
> Attribute lookup on this class never fails, as all attributes that are not actually set, have `None` value. The special attribute `__notes__` (used by `add_note` since Python 3.11) is explicitly excluded from this behavior to ensure compatibility.

> [!NOTE]
>
> **Example**
>
> ```python
> try:
>     if condition:
>         raise Error("Error message", err_code=1)
>     else:
>         raise Error("Unknown error")
> except Error as e:
>     if e.err_code is None:
>         ...
>     elif e.err_code == 1:
>         ...
> ```

> [!NOTE]
>
> Warnings are not errors and should typically derive from Warning, not this class.

## Singletons

Singleton is a pattern that restricts the instantiation of a class to one "single" instance. This is useful when exactly one object is needed to coordinate actions across the system.

Common uses:

- The abstract factory, factory method, builder, and prototype patterns can use singletons in their implementation.
- Facade objects are often singletons because only one facade object is required.
- State objects are often singletons.
- Singletons are often preferred to global variables because:
- They do not pollute the global namespace with unnecessary variables.
- They permit lazy allocation and initialization.

To create your own singletons, use Singleton as the base class.

> [!NOTE]
>
> **Example**
>
> ```python
> >>> class MySingleton(Singleton):
> ...     "Description"
> ...     ...
> ...
> >>> obj1 = MySingleton()
> >>> obj2 = MySingleton()
> >>> obj1 is obj2
> True
> ```

## `firebird.base.types.Singleton`

Base class for singletons.

Ensures that only one instance of a class derived from Singleton exists. Subsequent attempts to 'create' an instance will return the existing one.

> [!IMPORTANT]
>
> If a descendant class's `__init__` method accepts arguments, these arguments are only used the **first** time the instance is created. Subsequent calls that retrieve the cached instance will **not** invoke `__init__` again.

> [!NOTE]
>
> **Example**
>
> ```python
> class MyService(Singleton):
>     def __init__(self, config_param=None):
>         if hasattr(self, '_initialized'): # Prevent re-init
>             return
>         print("Initializing MyService...")
>         self.config = config_param
>         self._initialized = True
>
>     def do_something(self):
>         print(f"Doing something with config: {self.config}")
>
> service1 = MyService("config1") # Prints "Initializing MyService..."
> service2 = MyService("config2") # Does *not* print, returns existing instance
>
> print(service1 is service2) # Output: True
> service2.do_something()     # Output: Doing something with config: config1
> ```

## Sentinels

The Sentinel Object pattern is a standard Pythonic approach that’s used both in the Standard Library and beyond. The pattern most often uses Python’s built-in `None` object, but in situations where None might be a useful value, a unique sentinel `object()` can be used instead to indicate missing or unspecified data, or other specific condition.

However, the plain `object()` sentinel has not very useful `str` and `repr` values. The Sentinel class provides named sentinels, with meaningful `str` and `repr`.

## `firebird.base.types.Sentinel`

Bases: `_SentinelMeta`

Base class for creating unique sentinel objects.

Sentinels are special singleton objects used to signal unique states or conditions, particularly useful when `None` might be a valid data value. They offer a more explicit and readable alternative to magic constants or using `object()`.

You can define specific sentinels in two primary ways:

1. **By Subclassing:** Inherit directly from `Sentinel`. The name of the subclass becomes the sentinel's identity.

   ```python
   class DEFAULT(Sentinel):
       "Represents a default value placeholder."

   class ALL(Sentinel):
       "Represents all possible values."
   ```

   This creates classes `DEFAULT` and `ALL`, each acting as a unique sentinel object.
2. **Using the Functional Call:** Use the `Sentinel` base class itself as a factory function.

   ```python
   # Signature: Sentinel(name: str, *, repr: str | None = None) -> Sentinel
   NOT_FOUND = Sentinel("NOT_FOUND", repr="<Value Not Found>")
   UNKNOWN = Sentinel("UNKNOWN")
   ```

   - The required `name` argument (e.g., `"NOT_FOUND"`) specifies the `__name__` of the dynamically created sentinel class.
   - The optional `repr` keyword argument provides a custom string to be returned by `repr()` for this specific sentinel. If omitted, `repr()` defaults to the sentinel's name.

   This dynamically creates new classes derived from `Sentinel`, assigns them to the variables (`NOT_FOUND`, `UNKNOWN`), and sets a custom `__repr__` if provided.

**Behavior:**

Regardless of the creation method:

- Each sentinel is a unique object (a class behaving as a singleton).
- Sentinels are identified using the `is` operator.
- They cannot be instantiated (e.g., `DEFAULT()` raises TypeError).
- They cannot be subclassed further after their initial definition.
- `str(MySentinel)` returns the sentinel's name (`MySentinel.__name__`).
- `repr(MySentinel)` returns the custom `repr` if provided via the functional call, otherwise it defaults to the sentinel's name.

**Example Usage:**

```python
# Define using subclassing
class DEFAULT_SETTING(Sentinel):
    "Indicates a setting should use its compiled-in default."

# Define using functional call with custom repr
NOT_APPLICABLE = Sentinel("NOT_APPLICABLE", repr="<N/A>")

def get_config(key, user_override=NOT_APPLICABLE):
    if user_override is NOT_APPLICABLE:
        # User did not provide an override, check stored config
        value = read_stored_config(key, default=DEFAULT_SETTING)
        if value is DEFAULT_SETTING:
            return get_hardcoded_default(key)
        return value
    else:
        # User provided an override (which could be None)
        return user_override

config1 = get_config("timeout") # Uses stored or hardcoded default
config2 = get_config("retries", user_override=None) # Explicitly set to None
config3 = get_config("feature_flag", user_override=NOT_APPLICABLE) # Same as providing nothing

print(repr(DEFAULT_SETTING)) # Output: DEFAULT_SETTING
print(repr(NOT_APPLICABLE))  # Output: <N/A>
```

### Predefined sentinels

## `firebird.base.types.DEFAULT`

Bases: `Sentinel`

Sentinel that denotes default value

## `firebird.base.types.INFINITY`

Bases: `Sentinel`

Sentinel that denotes infinity value

## `firebird.base.types.UNLIMITED`

Bases: `Sentinel`

Sentinel that denotes unlimited value

## `firebird.base.types.UNKNOWN`

Bases: `Sentinel`

Sentinel that denotes unknown value

## `firebird.base.types.NOT_FOUND`

Bases: `Sentinel`

Sentinel that denotes a condition when value was not found

## `firebird.base.types.UNDEFINED`

Bases: `Sentinel`

Sentinel that denotes explicitly undefined value

## `firebird.base.types.ANY`

Bases: `Sentinel`

Sentinel that denotes any value

## `firebird.base.types.ALL`

Bases: `Sentinel`

Sentinel that denotes all possible values

## `firebird.base.types.SUSPEND`

Bases: `Sentinel`

Sentinel that denotes suspend request (in message queue)

## `firebird.base.types.RESUME`

Bases: `Sentinel`

Sentinel that denotes resume request (in message queue)

## `firebird.base.types.STOP`

Bases: `Sentinel`

Sentinel that denotes stop request (in message queue)

## Distinct objects

Some complex data structures or data processing algorithms require unique object identification (ie object identity). In Python, an object identity is defined internally as unique instance identity that is not suitable for complex objects whose identity is derived from content.

The Distinct abstract base class is intended as a unified solution to these needs.

> [!NOTE]
>
> **Seealso**
>
> module firebird.base.collections

## `firebird.base.types.Distinct`

Bases: `ABC`

Abstract base class for objects with distinct instances based on a key.

Instances are considered equal (`==`) if their keys, returned by `get_key()`, are equal. The hash of an instance is derived from the hash of its key by default.

> [!IMPORTANT]
>
> If used with `@dataclass`, it must be defined with `eq=False` to prevent overriding the custom `__eq__` and `__hash__` methods:
>
> Example:
>
> ```python
> from dataclasses import dataclass
>
> @dataclass(eq=False)
> class MyDistinctData(Distinct):
>     id: int
>     name: str
>
>     def get_key(self) -> Hashable:
>         return self.id
> ```

### `get_key() -> Hashable`

Return the unique key identifying this instance.

The key must be hashable. It determines equality and hashing behavior unless `__eq__` or `__hash__` are explicitly overridden.

## `firebird.base.types.CachedDistinct`

Bases: `Distinct`

Abstract Distinct descendant that caches instances.

Behaves like Distinct, but ensures only one instance is created per unique key. Subsequent attempts to create an instance with the same key (as determined by `extract_key` from the constructor arguments) will return the cached instance instead of creating a new one.

Instances are stored in a class-level weakref.WeakValueDictionary, allowing them to be garbage-collected if no longer referenced elsewhere.

Requires implementation of both `get_key()` (for instance equality/hashing) and `extract_key()` (for retrieving the key from constructor arguments **before** instance creation). These two methods should conceptually return the same identifier for a given object identity.

> [!IMPORTANT]
>
> Like `Distinct`, if used with `@dataclass`, define with `eq=False`.

> [!NOTE]
>
> **Example**
>
> ```python
> from dataclasses import dataclass
>
> @dataclass(eq=False) # Important!
> class User(CachedDistinct):
>     user_id: int
>     name: str
>
>     def get_key(self) -> int:
>         return self.user_id
>
>     @classmethod
>     def extract_key(cls, user_id: int, name: str) -> int:
>         # Extracts the key from __init__ args
>         return user_id
>
> user1 = User(1, "Alice")
> user2 = User(2, "Bob")
> user3 = User(1, "Alice") # Name might be different here, but key is the same
>
> print(user1 is user3)  # Output: True (cached instance returned)
> print(user1 == user3)  # Output: True (equality based on get_key)
> print(user1 is user2)  # Output: False
> ```

### `extract_key(*args, **kwargs) -> Hashable`

Returns key from arguments passed to `__init__()`.

> [!IMPORTANT]
>
> The key is used to store instance in cache. It should be the same as key returned by instance `Distinct.get_key()`!

### `get_key() -> Hashable`

Return the unique key identifying this instance.

The key must be hashable. It determines equality and hashing behavior unless `__eq__` or `__hash__` are explicitly overridden.

## Enums

## `firebird.base.types.ByteOrder`

Bases: `Enum`

Byte order for storing numbers in binary MemoryBuffer.

## `firebird.base.types.ZMQTransport`

Bases: `IntEnum`

ZeroMQ transport protocol.

## `firebird.base.types.ZMQDomain`

Bases: `IntEnum`

ZeroMQ address domain.

## Custom string types

Some string values have unified structure and carry specific information (like network address or database connection string). Typical repeating operation with these values are validation and parsing. It makes sense to put these operations under one roof. One such approach uses custom descendants of builtin `str` type.

> [!CAUTION]
>
> Custom string types have an inherent weakness. They support all inherited string methods, but any method that returns string value return a base `str` type, not the descendant class type. That same apply when you assign strings to variables that should be of custom string type.
>
> > **Tip**
> >
> > Module firebird.base.strconv could help you to safely translate strings stored externally to typed strings.

## `firebird.base.types.ZMQAddress`

Bases: `str`

ZeroMQ endpoint address.

It behaves like `str`, but checks that value is valid ZMQ endpoint address, has additional R/O properties and meaningful `repr()`.

Raises:

| Type | Description |
| --- | --- |
| `ValueError` | When string value passed to constructor is not a valid ZMQ endpoint address. |

> [!NOTE]
>
> **Example**
>
> ```python
> addr_str = "tcp://127.0.0.1:5555"
> zmq_addr = ZMQAddress(addr_str)
>
> print(zmq_addr)                 # Output: tcp://127.0.0.1:5555
> print(repr(zmq_addr))           # Output: ZMQAddress('tcp://127.0.0.1:5555')
> print(zmq_addr.protocol)        # Output: ZMQTransport.TCP
> print(zmq_addr.address)         # Output: 127.0.0.1:5555
> print(zmq_addr.domain)          # Output: ZMQDomain.NODE
>
> try:
>     invalid = ZMQAddress("myfile.txt")
> except ValueError as e:
>     print(e)                    # Output: Protocol specification required
> ```

### `address: str`

Endpoint address part (following '://').

### `domain: ZMQDomain`

Endpoint address domain (LOCAL, NODE, NETWORK).

### `protocol: ZMQTransport`

Transport protocol (e.g., TCP, IPC, INPROC).

## `firebird.base.types.MIME`

Bases: `str`

MIME type specification string (e.g., `text/plain; charset=utf-8`).

Behaves like `str`, but validates the input format (`type/subtype[;params]`) upon creation and provides convenient read-only properties to access parts of the specification.

Raises:

| Type | Description |
| --- | --- |
| `ValueError` | If the input string is not a valid MIME type specification (missing '/', unsupported type, invalid parameters). |

> [!NOTE]
>
> **Example**
>
> ```python
> mime1_str = "application/json"
> mime1 = MIME(mime1_str)
> print(mime1)            # Output: application/json
> print(repr(mime1))      # Output: MIME('application/json')
> print(mime1.type)       # Output: application
> print(mime1.subtype)    # Output: json
> print(mime1.params)     # Output: {}
>
> mime2_str = "text/html; charset=UTF-8"
> mime2 = MIME(mime2_str)
> print(mime2.mime_type)  # Output: text/html
> print(mime2.params)     # Output: {'charset': 'UTF-8'}
>
> try:
>     invalid_mime = MIME("application")
> except ValueError as e:
>     print(e) # Output: MIME type specification must be 'type/subtype[;param=value;...]'
>
> try:
>     invalid_mime = MIME("myapp/data") # 'myapp' is not a standard type
> except ValueError as e:
>     print(e) # Output: MIME type 'myapp' not supported
> ```

### `mime_type: str`

The base MIME type specification: '/'.

### `params: dict[str, str]`

MIME parameters as a dictionary (e.g., {'charset': 'utf-8'}).

### `subtype: str`

The MIME subtype (e.g., 'plain', 'json').

### `type: str`

The main MIME type (e.g., 'text', 'application').

## `firebird.base.types.PyExpr`

Bases: `str`

Source code string representing a single Python expression.

Behaves like `str`, but validates that the content is a syntactically valid Python expression during initialization by attempting to compile it in `eval` mode. Provides access to the compiled code object and a helper to create a callable function from the expression.

Raises:

| Type | Description |
| --- | --- |
| `SyntaxError` | If the string value is not a valid Python expression. |

> [!NOTE]
>
> **Example**
>
> ```python
> expr_str = "a + b * 2"
> py_expr = PyExpr(expr_str)
>
> print(py_expr)           # Output: a + b * 2
> print(repr(py_expr))     # Output: PyExpr('a + b * 2')
>
> # Get the compiled code object
> code_obj = py_expr.expr
> print(eval(code_obj, {'a': 10, 'b': 5})) # Output: 20
>
> # Get a callable function
> func = py_expr.get_callable(arguments='a, b')
> print(func(a=3, b=4))    # Output: 11
>
> # Using a namespace
> import math
> log_expr = PyExpr("math.log10(x)")
> log_func = log_expr.get_callable(arguments='x', namespace={'math': math})
> print(log_func(x=100))   # Output: 2.0
>
> try:
>     invalid_expr = PyExpr("a = 5") # Assignment is not an expression
> except SyntaxError as e:
>     print(e)              # Output: invalid syntax (<string>, line 1) or similar
> ```

### `expr: types.CodeType`

The compiled expression code object, ready for `eval()`.

### `get_callable(arguments: str = '', namespace: dict[str, Any] | None = None) -> Callable`

Returns the expression wrapped in a callable function.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `arguments` | `str` | Comma-separated string of argument names for the function signature. | `''` |
| `namespace` | `dict[str, Any] \| None` | Optional dictionary providing the execution namespace for the expression. Can be used to provide access to modules or specific values. | `None` |

Returns:

| Type | Description |
| --- | --- |
| `Callable` | A callable function that takes the specified arguments and returns |
| `Callable` | the result of evaluating the expression. |

## `firebird.base.types.PyCode`

Bases: `str`

Source code string representing a block of Python statements.

Behaves like `str`, but validates that the content is a syntactically valid Python code block (potentially multiple statements) during initialization by attempting to compile it in `exec` mode. Provides access to the compiled code object.

Raises:

| Type | Description |
| --- | --- |
| `SyntaxError` | If the string value is not a valid Python code block. |

> [!NOTE]
>
> **Example**
>
> ```python
> code_str = '''
> import math
> result = math.sqrt(x * y)
> print(f"Result: {result}")
> '''
> py_code = PyCode(code_str)
>
> print(py_code[:20])      # Output: import math\nresult
> print(repr(py_code))     # Output: PyCode('import math\nresult = ...')
>
> # Get the compiled code object
> code_obj = py_code.code
>
> # Execute the code block
> exec_namespace = {'x': 4, 'y': 9}
> exec(code_obj, exec_namespace) # Output: Result: 6.0
> print(exec_namespace['result']) # Output: 6.0
>
> try:
>     # Invalid syntax (e.g., unmatched parenthesis)
>     invalid_code = PyCode("print('Hello'")
> except SyntaxError as e:
>     print(e)             # Output: unexpected EOF while parsing (<string>, line 1) or similar
> ```

### `code: types.CodeType`

The compiled Python code object, ready for `exec()`.

## `firebird.base.types.PyCallable`

Bases: `str`

Source code string representing a Python callable (function or class definition).

Behaves like `str`, but validates that the content is a syntactically valid Python function or class definition during initialization. It compiles and executes the definition to capture the resulting callable object.

Instances of PyCallable are themselves callable, acting as a proxy to the defined function or class.

Raises:

| Type | Description |
| --- | --- |
| `ValueError` | If the string does not contain a recognizable `def` or `class` definition at the top level. |
| `SyntaxError` | If the string contains syntactically invalid Python code. |
| `NameError` | If the definition relies on names not available during its execution. |

> [!NOTE]
>
> **Example**
>
> ```python
> func_str = '''
> def greet(name):
>     "Greets the person."
>     return f"Hello, {name}!"
> '''
> py_func = PyCallable(func_str)
>
> print(py_func.name)      # Output: greet
> print(py_func.__doc__)   # Output: Greets the person.
> print(repr(py_func))     # Output: PyCallable('def greet(name):\n    ...')
>
> # Call the instance directly
> message = py_func(name="World")
> print(message)           # Output: Hello, World!
>
> class_str = '''
> class MyNumber:
>     def __init__(self, value):
>         self.value = value
>     def double(self):
>         return self.value * 2
> '''
> py_class = PyCallable(class_str)
>
> print(py_class.name)     # Output: MyNumber
> instance = py_class(value=10) # Instantiate the class via the PyCallable object
> print(instance.double()) # Output: 20
>
> try:
>     # Missing 'def' or 'class'
>     invalid = PyCallable("print('Hello')")
> except ValueError as e:
>     print(e)             # Output: Python function or class definition not found
>
> try:
>     # Syntax error in definition
>     invalid = PyCallable("def my_func(x:")
> except SyntaxError as e:
>     print(e)             # Output: invalid syntax (<string>, line 1) or similar
> ```

### `__call__(*args, **kwargs) -> Any`

Calls the wrapped function or instantiates the wrapped class.

## Meta classes

## `firebird.base.types.SingletonMeta`

Bases: `type`

Metaclass for Singleton classes.

Manages internal cache of class instances. If instance for a class is in cache, it's returned without calling the constructor, otherwise the instance is created normally and stored in cache for later use.

## `firebird.base.types._SentinelMeta`

Bases: `type`

Metaclass for Sentinel objects.

This metaclass ensures that classes defined using it behave as proper sentinels:

- They cannot be instantiated directly (e.g., `MySentinel()`).
- They cannot be subclassed after initial definition.
- Provides a basic `__repr__` and `__str__` based on the class name.
- Allows defining sentinels via class definition (`class NAME(Sentinel): ...`) or potentially a functional call (though class definition is preferred).
- Neuters `__call__` inherited from `type` to prevent unintended behavior.

## `firebird.base.types.CachedDistinctMeta`

Bases: `ABCMeta`

Metaclass for `CachedDistinct`.

Intercepts class instantiation (`__call__`) to implement the instance caching mechanism based on the key extracted by `cls.extract_key()`. Ensures that only one instance exists per unique key.

## `firebird.base.types.conjunctive(name, bases, attrs) -> type`

Returns a metaclass that is conjunctive descendant of all metaclasses used by parent classes. It's necessary to create a class with multiple inheritance, where multiple parent classes use different metaclasses.

> [!NOTE]
>
> **Example**
>
> ```python
> class A(type): pass
>
> class B(type): pass
>
> class AA(metaclass=A):pass
>
> class BB(metaclass=B):pass
>
> class CC(AA, BB, metaclass=Conjunctive): pass
> ```

## Functions

## `firebird.base.types.load(spec: str) -> Any`

Dynamically load an object (class, function, variable) from a module.

The module is imported automatically if it hasn't been already.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `spec` | `str` | Object specification string in the format `'module[.submodule...]:object_name[.attribute...]'`. | *required* |

Returns:

| Type | Description |
| --- | --- |
| `Any` | The loaded object. |

Raises:

| Type | Description |
| --- | --- |
| `ImportError` | If the module cannot be imported. |
| `AttributeError` | If the specified object cannot be found within the module. |

> [!NOTE]
>
> **Example**
>
> ```python
> # Assuming 'my_package/my_module.py' contains: class MyClass: pass
> MyClassRef = load("my_package.my_module:MyClass")
> instance = MyClassRef()
>
> # Load a function
> pprint_func = load("pprint:pprint")
> pprint_func({"a": 1})
> ```
