lumera 0.26.0__tar.gz → 0.28.0.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {lumera-0.26.0 → lumera-0.28.0.dev1}/PKG-INFO +2 -2
  2. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/__init__.py +7 -0
  3. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/_utils.py +20 -17
  4. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/flags.py +2 -2
  5. lumera-0.28.0.dev1/lumera/functions/__init__.py +55 -0
  6. lumera-0.28.0.dev1/lumera/functions/__main__.py +5 -0
  7. lumera-0.28.0.dev1/lumera/functions/_bundle.py +194 -0
  8. lumera-0.28.0.dev1/lumera/functions/_core.py +382 -0
  9. lumera-0.28.0.dev1/lumera/functions/_runner.py +163 -0
  10. lumera-0.28.0.dev1/lumera/integrations/netsuite.py +474 -0
  11. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/pb.py +24 -1
  12. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/sdk.py +54 -0
  13. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera.egg-info/PKG-INFO +2 -2
  14. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera.egg-info/SOURCES.txt +7 -0
  15. {lumera-0.26.0 → lumera-0.28.0.dev1}/pyproject.toml +2 -2
  16. {lumera-0.26.0 → lumera-0.28.0.dev1}/tests/test_flags.py +1 -1
  17. lumera-0.28.0.dev1/tests/test_functions.py +528 -0
  18. lumera-0.28.0.dev1/tests/test_integrations_netsuite.py +330 -0
  19. {lumera-0.26.0 → lumera-0.28.0.dev1}/tests/test_sdk.py +19 -1
  20. lumera-0.26.0/lumera/integrations/netsuite.py +0 -230
  21. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/_live.py +0 -0
  22. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/agents.py +0 -0
  23. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/automations.py +0 -0
  24. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/email.py +0 -0
  25. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/exceptions.py +0 -0
  26. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/files.py +0 -0
  27. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/google.py +0 -0
  28. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/integrations/__init__.py +0 -0
  29. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/integrations/bill.py +0 -0
  30. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/integrations/google.py +0 -0
  31. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/integrations/salesforce.py +0 -0
  32. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/integrations/slack.py +0 -0
  33. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/llm.py +0 -0
  34. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/storage.py +0 -0
  35. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera/webhooks.py +0 -0
  36. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera.egg-info/dependency_links.txt +0 -0
  37. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera.egg-info/requires.txt +1 -1
  38. {lumera-0.26.0 → lumera-0.28.0.dev1}/lumera.egg-info/top_level.txt +0 -0
  39. {lumera-0.26.0 → lumera-0.28.0.dev1}/setup.cfg +0 -0
  40. {lumera-0.26.0 → lumera-0.28.0.dev1}/tests/test_agents_live.py +0 -0
  41. {lumera-0.26.0 → lumera-0.28.0.dev1}/tests/test_automations.py +0 -0
  42. {lumera-0.26.0 → lumera-0.28.0.dev1}/tests/test_integrations_salesforce.py +0 -0
  43. {lumera-0.26.0 → lumera-0.28.0.dev1}/tests/test_webhooks.py +0 -0
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lumera
3
- Version: 0.26.0
3
+ Version: 0.28.0.dev1
4
4
  Summary: SDK for building on Lumera platform
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: requests
7
7
  Requires-Dist: python-dotenv
8
8
  Requires-Dist: google-api-python-client==2.173.0
9
+ Requires-Dist: pydantic<3.0.0,>=2.12.3
9
10
  Requires-Dist: simple-salesforce==1.12.6
10
11
  Requires-Dist: slack_sdk==3.35.0
11
12
  Provides-Extra: dev
@@ -28,7 +29,6 @@ Requires-Dist: openai-agents<1.0.0,>=0.6.5; extra == "full"
28
29
  Requires-Dist: openpyxl==3.1.5; extra == "full"
29
30
  Requires-Dist: pandas==2.3.0; extra == "full"
30
31
  Requires-Dist: pdfplumber; extra == "full"
31
- Requires-Dist: pydantic<3.0.0,>=2.12.3; extra == "full"
32
32
  Requires-Dist: PyGithub==2.6.1; extra == "full"
33
33
  Requires-Dist: python-dotenv==1.1.0; extra == "full"
34
34
  Requires-Dist: pyzmq; extra == "full"
@@ -18,6 +18,7 @@ from . import (
18
18
  automations,
19
19
  email,
20
20
  exceptions,
21
+ functions,
21
22
  integrations,
22
23
  llm,
23
24
  pb,
@@ -48,6 +49,7 @@ from .exceptions import (
48
49
  # Import file types for Pydantic automation inputs
49
50
  from .files import LumeraFile, LumeraFiles
50
51
  from .flags import feature_flag, feature_flags, is_feature_enabled
52
+ from .functions import function
51
53
 
52
54
  # Import key SDK helpers to expose them at the package root.
53
55
  from .sdk import (
@@ -67,6 +69,7 @@ from .sdk import (
67
69
  replay_hook,
68
70
  run_automation,
69
71
  save_to_lumera,
72
+ search_records,
70
73
  update_automation_run,
71
74
  update_collection,
72
75
  update_record,
@@ -88,6 +91,8 @@ __all__ = [
88
91
  "feature_flag",
89
92
  "feature_flags",
90
93
  "is_feature_enabled",
94
+ # Callable Function authoring
95
+ "function",
91
96
  # Collections (low-level API)
92
97
  "list_collections",
93
98
  "get_collection",
@@ -96,6 +101,7 @@ __all__ = [
96
101
  "delete_collection",
97
102
  # Records (low-level API)
98
103
  "list_records",
104
+ "search_records",
99
105
  "get_record",
100
106
  "get_record_by_external_id",
101
107
  "create_record",
@@ -131,6 +137,7 @@ __all__ = [
131
137
  "storage",
132
138
  "llm",
133
139
  "exceptions",
140
+ "functions",
134
141
  "webhooks",
135
142
  "integrations",
136
143
  ]
@@ -20,12 +20,18 @@ from dotenv import load_dotenv
20
20
 
21
21
  TOKEN_ENV = "LUMERA_TOKEN"
22
22
  BASE_URL_ENV = "LUMERA_BASE_URL"
23
+ CREDENTIALS_PATH_ENV = "LUMERA_CREDENTIALS_PATH"
23
24
  EXPECTED_TEST_ERROR_ENV = "LUMERA_EXPECTED_TEST_ERROR"
24
25
  EXPECTED_TEST_HEADER = "X-Lumera-Expected-Test"
25
26
  ENV_PATH = "/root/.env"
26
27
  LOCAL_CREDS_PATH = ".lumera/credentials.json"
27
28
  GLOBAL_CREDS_PATH = os.path.join(os.path.expanduser("~"), ".config", "lumera", "credentials.json")
28
29
 
30
+
31
+ def _credentials_paths() -> tuple[str, ...]:
32
+ explicit = os.getenv(CREDENTIALS_PATH_ENV, "").strip()
33
+ return tuple(dict.fromkeys(path for path in (explicit, LOCAL_CREDS_PATH, GLOBAL_CREDS_PATH) if path))
34
+
29
35
  load_dotenv(override=False)
30
36
  load_dotenv(ENV_PATH, override=False)
31
37
 
@@ -37,7 +43,7 @@ def _resolve_api_base() -> str:
37
43
 
38
44
  Priority:
39
45
  1. LUMERA_BASE_URL environment variable
40
- 2. base_url from credentials file (project-local then global)
46
+ 2. base_url from credentials file (explicit runtime, project-local, global)
41
47
  3. Default (https://app.lumerahq.com/api)
42
48
  """
43
49
  # 1. Environment variable
@@ -47,7 +53,7 @@ def _resolve_api_base() -> str:
47
53
  return base if base.endswith("/api") else base + "/api"
48
54
 
49
55
  # 2. Credentials file
50
- for path in (LOCAL_CREDS_PATH, GLOBAL_CREDS_PATH):
56
+ for path in _credentials_paths():
51
57
  try:
52
58
  if os.path.exists(path):
53
59
  with open(path) as f:
@@ -112,30 +118,27 @@ def get_lumera_token() -> str:
112
118
 
113
119
  Priority:
114
120
  1. LUMERA_TOKEN environment variable
115
- 2. .lumera/credentials.json (project-local, from CLI login --local)
116
- 3. ~/.config/lumera/credentials.json (global, from CLI login)
117
- 4. /root/.env (legacy, for automations in sandbox)
121
+ 2. LUMERA_CREDENTIALS_PATH (sandbox runtime credentials)
122
+ 3. .lumera/credentials.json (project-local, from CLI login --local)
123
+ 4. ~/.config/lumera/credentials.json (global, from CLI login)
124
+ 5. /root/.env (legacy, for automations in sandbox)
118
125
  """
119
126
  # 1. Check environment variable (highest priority)
120
127
  token = os.getenv(TOKEN_ENV)
121
128
  if token:
122
129
  return token
123
130
 
124
- # 2. Check project-local credentials
125
- token = _read_token_from_creds_file(LOCAL_CREDS_PATH)
126
- if token:
127
- return token
128
-
129
- # 3. Check global credentials
130
- token = _read_token_from_creds_file(GLOBAL_CREDS_PATH)
131
- if token:
132
- return token
131
+ # 2-4. Check explicit runtime, project-local, then global credentials.
132
+ for path in _credentials_paths():
133
+ token = _read_token_from_creds_file(path)
134
+ if token:
135
+ return token
133
136
 
134
- # 4. /root/.env is already loaded via dotenv at module load time
137
+ # 5. /root/.env is already loaded via dotenv at module load time
135
138
  # so if we get here, no token was found anywhere
136
139
  raise RuntimeError(
137
- f"{TOKEN_ENV} not found. Checked: environment, {LOCAL_CREDS_PATH}, "
138
- f"{GLOBAL_CREDS_PATH}, {ENV_PATH}. "
140
+ f"{TOKEN_ENV} not found. Checked: environment, {_credentials_paths()}, "
141
+ f"{ENV_PATH}. "
139
142
  f"Run `lumera login` to authenticate."
140
143
  )
141
144
 
@@ -4,7 +4,7 @@ Feature flags inside Lumera sandboxes.
4
4
  The Lumera backend resolves the company's feature flags at sandbox warm and
5
5
  writes a typed JSON snapshot to a well-known path (pointed to by the
6
6
  ``LUMERA_FEATURE_FLAGS_PATH`` env var, default
7
- ``/opt/lumera/feature-flags/flags.json``).
7
+ ``/lumera_state/feature-flags/flags.json``).
8
8
  This module reads that snapshot so sandbox code can gate behavior on a flag.
9
9
 
10
10
  Values are stored in their native type (booleans, numbers, JSON objects), so
@@ -35,7 +35,7 @@ _logger = logging.getLogger("lumera.sdk")
35
35
 
36
36
  # Keep in sync with backend sandboximage.FeatureFlagsPath / FeatureFlagsPathEnv.
37
37
  _ENV_PATH = "LUMERA_FEATURE_FLAGS_PATH"
38
- _DEFAULT_PATH = "/opt/lumera/feature-flags/flags.json"
38
+ _DEFAULT_PATH = "/lumera_state/feature-flags/flags.json"
39
39
 
40
40
  _lock = threading.Lock()
41
41
  # Cache entry: (cache_key, flags). cache_key is (path, mtime_ns) so a refreshed
@@ -0,0 +1,55 @@
1
+ """Define, discover, inspect, and locally invoke typed Lumera Functions."""
2
+
3
+ from ._bundle import (
4
+ FUNCTION_BUNDLE_FORMAT_VERSION,
5
+ FUNCTION_BUNDLE_RUNTIME,
6
+ FunctionBundle,
7
+ build_bundle,
8
+ )
9
+ from ._core import (
10
+ FUNCTION_MANIFEST_SCHEMA_VERSION,
11
+ DuplicateFunctionError,
12
+ FunctionConfirmation,
13
+ FunctionDefinition,
14
+ FunctionDefinitionError,
15
+ FunctionDiscoveryError,
16
+ FunctionEffect,
17
+ FunctionError,
18
+ FunctionExecutionError,
19
+ FunctionMetadata,
20
+ FunctionNotFoundError,
21
+ FunctionOutputSerializationError,
22
+ FunctionValidationError,
23
+ build_manifest,
24
+ discover_modules,
25
+ function,
26
+ get_function_definition,
27
+ invoke_function,
28
+ list_function_definitions,
29
+ )
30
+
31
+ __all__ = [
32
+ "FUNCTION_BUNDLE_FORMAT_VERSION",
33
+ "FUNCTION_BUNDLE_RUNTIME",
34
+ "FUNCTION_MANIFEST_SCHEMA_VERSION",
35
+ "DuplicateFunctionError",
36
+ "FunctionConfirmation",
37
+ "FunctionDefinition",
38
+ "FunctionDefinitionError",
39
+ "FunctionDiscoveryError",
40
+ "FunctionEffect",
41
+ "FunctionError",
42
+ "FunctionExecutionError",
43
+ "FunctionBundle",
44
+ "FunctionMetadata",
45
+ "FunctionNotFoundError",
46
+ "FunctionOutputSerializationError",
47
+ "FunctionValidationError",
48
+ "build_manifest",
49
+ "build_bundle",
50
+ "discover_modules",
51
+ "function",
52
+ "get_function_definition",
53
+ "invoke_function",
54
+ "list_function_definitions",
55
+ ]
@@ -0,0 +1,5 @@
1
+ """CLI entry point for ``python -m lumera.functions``."""
2
+
3
+ from ._runner import main
4
+
5
+ raise SystemExit(main())
@@ -0,0 +1,194 @@
1
+ """Build immutable, deterministic deployment bundles for Lumera Functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import gzip
6
+ import hashlib
7
+ import io
8
+ import json
9
+ import os
10
+ import sys
11
+ import tarfile
12
+ from collections.abc import Iterator, Sequence
13
+ from contextlib import contextmanager
14
+ from dataclasses import dataclass
15
+ from pathlib import Path, PurePosixPath
16
+ from typing import Any
17
+
18
+ from ._core import build_manifest, discover_modules
19
+
20
+ FUNCTION_BUNDLE_FORMAT_VERSION = 1
21
+ FUNCTION_BUNDLE_RUNTIME = "python3.13"
22
+
23
+ _SOURCE_FILENAMES = {
24
+ "pyproject.toml",
25
+ "requirements.txt",
26
+ "requirements.lock",
27
+ "uv.lock",
28
+ }
29
+ _EXCLUDED_DIRECTORY_NAMES = {
30
+ ".git",
31
+ ".mypy_cache",
32
+ ".pytest_cache",
33
+ ".ruff_cache",
34
+ ".venv",
35
+ "__pycache__",
36
+ "node_modules",
37
+ }
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class FunctionBundle:
42
+ """Metadata for one built Function deployment bundle."""
43
+
44
+ path: Path
45
+ sha256: str
46
+ size_bytes: int
47
+ manifest: dict[str, Any]
48
+ modules: tuple[str, ...]
49
+ source_files: tuple[str, ...]
50
+
51
+ def to_dict(self) -> dict[str, Any]:
52
+ """Return a JSON-serializable build result."""
53
+
54
+ return {
55
+ "path": str(self.path),
56
+ "sha256": self.sha256,
57
+ "size_bytes": self.size_bytes,
58
+ "manifest": self.manifest,
59
+ "modules": list(self.modules),
60
+ "source_files": list(self.source_files),
61
+ }
62
+
63
+
64
+ def build_bundle(
65
+ project_root: str | os.PathLike[str],
66
+ module_names: Sequence[str],
67
+ output_path: str | os.PathLike[str],
68
+ ) -> FunctionBundle:
69
+ """Build a deterministic ``tar.gz`` containing source and Function metadata."""
70
+
71
+ root = Path(project_root).expanduser().resolve()
72
+ if not root.is_dir():
73
+ raise ValueError(f"Function project root is not a directory: {root}")
74
+
75
+ modules = _normalize_modules(module_names)
76
+ output = Path(output_path).expanduser().resolve()
77
+ output.parent.mkdir(parents=True, exist_ok=True)
78
+
79
+ with _prepend_sys_path(root):
80
+ discover_modules(modules)
81
+ manifest = build_manifest()
82
+
83
+ source_entries = _collect_source_entries(root)
84
+ manifest_bytes = _canonical_json_bytes(manifest)
85
+ build_metadata = {
86
+ "format_version": FUNCTION_BUNDLE_FORMAT_VERSION,
87
+ "runtime": FUNCTION_BUNDLE_RUNTIME,
88
+ "modules": list(modules),
89
+ "manifest_sha256": hashlib.sha256(manifest_bytes).hexdigest(),
90
+ "source_files": [
91
+ {
92
+ "path": relative_path,
93
+ "sha256": hashlib.sha256(content).hexdigest(),
94
+ "size_bytes": len(content),
95
+ }
96
+ for relative_path, content in source_entries
97
+ ],
98
+ }
99
+
100
+ archive_bytes = _build_archive(
101
+ manifest_bytes=manifest_bytes,
102
+ build_bytes=_canonical_json_bytes(build_metadata),
103
+ source_entries=source_entries,
104
+ )
105
+ output.write_bytes(archive_bytes)
106
+
107
+ return FunctionBundle(
108
+ path=output,
109
+ sha256=hashlib.sha256(archive_bytes).hexdigest(),
110
+ size_bytes=len(archive_bytes),
111
+ manifest=manifest,
112
+ modules=modules,
113
+ source_files=tuple(relative_path for relative_path, _ in source_entries),
114
+ )
115
+
116
+
117
+ def _normalize_modules(module_names: Sequence[str]) -> tuple[str, ...]:
118
+ modules = tuple(dict.fromkeys(name.strip() for name in module_names if name.strip()))
119
+ if not modules:
120
+ raise ValueError("at least one Function module is required")
121
+ return modules
122
+
123
+
124
+ def _collect_source_entries(root: Path) -> list[tuple[str, bytes]]:
125
+ entries: list[tuple[str, bytes]] = []
126
+ for candidate in sorted(root.rglob("*")):
127
+ relative = candidate.relative_to(root)
128
+ if any(part in _EXCLUDED_DIRECTORY_NAMES for part in relative.parts):
129
+ continue
130
+ if candidate.is_symlink() or not candidate.is_file():
131
+ continue
132
+ if candidate.suffix != ".py" and candidate.name not in _SOURCE_FILENAMES:
133
+ continue
134
+ relative_path = PurePosixPath(*relative.parts).as_posix()
135
+ entries.append((relative_path, candidate.read_bytes()))
136
+ if not entries:
137
+ raise ValueError(f"Function project contains no Python source files: {root}")
138
+ return entries
139
+
140
+
141
+ def _build_archive(
142
+ *,
143
+ manifest_bytes: bytes,
144
+ build_bytes: bytes,
145
+ source_entries: Sequence[tuple[str, bytes]],
146
+ ) -> bytes:
147
+ tar_buffer = io.BytesIO()
148
+ with tarfile.open(fileobj=tar_buffer, mode="w", format=tarfile.PAX_FORMAT) as archive:
149
+ _add_tar_file(archive, "manifest.json", manifest_bytes)
150
+ _add_tar_file(archive, "build.json", build_bytes)
151
+ for relative_path, content in source_entries:
152
+ _add_tar_file(archive, f"source/{relative_path}", content)
153
+
154
+ gzip_buffer = io.BytesIO()
155
+ with gzip.GzipFile(fileobj=gzip_buffer, mode="wb", filename="", mtime=0) as compressed:
156
+ compressed.write(tar_buffer.getvalue())
157
+ return gzip_buffer.getvalue()
158
+
159
+
160
+ def _add_tar_file(archive: tarfile.TarFile, name: str, content: bytes) -> None:
161
+ info = tarfile.TarInfo(name)
162
+ info.size = len(content)
163
+ info.mode = 0o644
164
+ info.mtime = 0
165
+ info.uid = 0
166
+ info.gid = 0
167
+ info.uname = ""
168
+ info.gname = ""
169
+ archive.addfile(info, io.BytesIO(content))
170
+
171
+
172
+ def _canonical_json_bytes(value: object) -> bytes:
173
+ return (
174
+ json.dumps(
175
+ value,
176
+ ensure_ascii=False,
177
+ separators=(",", ":"),
178
+ sort_keys=True,
179
+ )
180
+ + "\n"
181
+ ).encode()
182
+
183
+
184
+ @contextmanager
185
+ def _prepend_sys_path(root: Path) -> Iterator[None]:
186
+ root_value = str(root)
187
+ sys.path.insert(0, root_value)
188
+ try:
189
+ yield
190
+ finally:
191
+ try:
192
+ sys.path.remove(root_value)
193
+ except ValueError:
194
+ pass