Build an Extension Package¶
ZettelForge discovers installed extension packages at startup via zettelforge.extensions.load_extensions(). An extension is any Python package that registers itself under the zettelforge.extensions namespace or is importable as zettelforge_enterprise.
Prerequisites¶
- ZettelForge installed (
pip install zettelforge) - Python 3.12+
- For enterprise features: separate
zettelforge-enterprisepackage (not distributed on PyPI)
How Extensions Are Loaded¶
The extension loader in zettelforge.extensions follows a two-check discovery:
- Try importing
zettelforge_enterprise-- if the package is installed, it is loaded as the"enterprise"extension. - Legacy env var fallback -- if no package was found, check
THREATENGRAM_LICENSE_KEY. If it matches theTG-XXXX-XXXX-XXXX-XXXXpattern, a marker is stored sohas_extension("enterprise")returnsTrue.
from zettelforge.extensions import load_extensions, has_extension, get_extension
load_extensions()
print(has_extension("enterprise")) # True or False
The loader is idempotent -- subsequent calls return the cached result without re-scanning the environment.
Steps¶
1. Name your package¶
Use the zettelforge_ prefix to keep naming consistent and avoid collisions:
zettelforge_enterprise-- enterprise features (TypeDB, OpenCTI, telemetry)zettelforge_myfeature-- your custom feature
2. Create the package structure¶
The __init__.py can be empty -- the extension loader only needs the package to be importable.
3. Register as a ZettelForge extension (optional)¶
If you want your extension to be discoverable beyond the zettelforge_enterprise naming convention, register via a plugin entry point in pyproject.toml:
Then consumers can check for it by name:
from zettelforge.extensions import has_extension
if has_extension("myfeature"):
# activate custom behaviour
4. Respect the edition API¶
Use the zettelforge.edition module to gate features behind the active edition:
from zettelforge.edition import is_enterprise, EditionError
if not is_enterprise():
raise EditionError("This feature requires ZettelForge Enterprise")
Available edition functions:
| Function | Returns | Description |
|---|---|---|
is_enterprise() |
bool |
True if enterprise extensions are loaded |
is_community() |
bool |
True if no enterprise extensions |
get_edition() |
Edition |
Edition.ENTERPRISE or Edition.COMMUNITY |
edition_name() |
str |
"ZettelForge + Extensions" or "ZettelForge" |
5. Expose extension features¶
Your extension package should provide the actual feature implementations. The get_extension() function lets core code access your extension module:
from zettelforge.extensions import get_extension
enterprise = get_extension("enterprise")
if enterprise is not None:
# Access TypeDB backend, OpenCTI sync, telemetry, etc.
enterprise.register_backends()
6. Test your extension¶
Use the reset_extensions() function in setup/teardown to clear cached state between tests:
import os
from unittest.mock import patch
from zettelforge.extensions import load_extensions, has_extension, reset_extensions
def test_extension_loaded():
reset_extensions()
# Simulate having the enterprise package
with patch.dict("sys.modules", {"zettelforge_enterprise": __import__("types")}):
load_extensions()
assert has_extension("enterprise") is True
def test_extension_not_loaded():
reset_extensions()
# Simulate missing package
with patch.dict("sys.modules", {"zettelforge_enterprise": None}):
load_extensions()
assert has_extension("enterprise") is False
def test_legacy_env_var_activates():
reset_extensions()
os.environ["THREATENGRAM_LICENSE_KEY"] = "TG-1234-5678-9abc-def0"
with patch.dict("sys.modules", {"zettelforge_enterprise": None}):
load_extensions()
assert has_extension("enterprise") is True
def test_invalid_env_var_does_not_activate():
reset_extensions()
os.environ["THREATENGRAM_LICENSE_KEY"] = "invalid-key"
with patch.dict("sys.modules", {"zettelforge_enterprise": None}):
load_extensions()
assert has_extension("enterprise") is False
def test_get_missing_returns_none():
reset_extensions()
with patch.dict("sys.modules", {"zettelforge_enterprise": None}):
assert get_extension("enterprise") is None
7. Use the optional-feature pattern for SDK dependencies¶
If your extension depends on an optional SDK (e.g., typedb-client, pycti), follow the optional-feature pattern:
class MyFeature:
def __init__(self):
self._sdk = None
self._lock = threading.Lock()
def _ensure_loaded(self):
if self._sdk is not None:
return
with self._lock:
if self._sdk is not None:
return
try:
import typedb # lazy import
except ImportError as exc:
raise ImportError(
"TypeDB feature requires typedb-client. "
"Install with: pip install zettelforge-enterprise"
) from exc
self._sdk = typedb
This ensures core ZettelForge never depends on your SDK, and the error surfaces only at the point of use.
LLM Quick Reference¶
Task: Create a ZettelForge extension package.
Key functions: load_extensions() (idempotent discovery), has_extension(name) (boolean check), get_extension(name) (module or None), reset_extensions() (test cleanup).
Edition module: is_enterprise(), is_community(), get_edition(), edition_name() let core code gate features behind edition.
Activation paths: Package import (zettelforge_enterprise) takes priority. Legacy env var (THREATENGRAM_LICENSE_KEY=TG-XXXX-XXXX-XXXX-XXXX) is the fallback for backward compatibility.
Test pattern: reset_extensions() in setup, patch.dict("sys.modules", ...) to control whether the package exists, patch.dict(os.environ, ...) for env var tests.
Optional SDK pattern: Lazy-import the SDK in a private _ensure_loaded() method. Never import at module level. Surface a clear ImportError with install instructions.
Entry point registration: Add [project.entry-points."zettelforge.extensions"] in pyproject.toml for discovery by name beyond the zettelforge_enterprise convention.