lumera 0.28.0.dev1__tar.gz → 0.28.0.dev2__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 (44) hide show
  1. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/PKG-INFO +2 -2
  2. lumera-0.28.0.dev2/lumera/__init__.py +175 -0
  3. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/integrations/netsuite.py +40 -3
  4. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera.egg-info/PKG-INFO +2 -2
  5. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera.egg-info/SOURCES.txt +1 -6
  6. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/pyproject.toml +2 -2
  7. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/tests/test_integrations_netsuite.py +27 -0
  8. lumera-0.28.0.dev2/tests/test_lazy_imports.py +230 -0
  9. lumera-0.28.0.dev1/lumera/__init__.py +0 -143
  10. lumera-0.28.0.dev1/lumera/functions/__init__.py +0 -55
  11. lumera-0.28.0.dev1/lumera/functions/__main__.py +0 -5
  12. lumera-0.28.0.dev1/lumera/functions/_bundle.py +0 -194
  13. lumera-0.28.0.dev1/lumera/functions/_core.py +0 -382
  14. lumera-0.28.0.dev1/lumera/functions/_runner.py +0 -163
  15. lumera-0.28.0.dev1/tests/test_functions.py +0 -528
  16. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/_live.py +0 -0
  17. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/_utils.py +0 -0
  18. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/agents.py +0 -0
  19. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/automations.py +0 -0
  20. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/email.py +0 -0
  21. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/exceptions.py +0 -0
  22. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/files.py +0 -0
  23. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/flags.py +0 -0
  24. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/google.py +0 -0
  25. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/integrations/__init__.py +0 -0
  26. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/integrations/bill.py +0 -0
  27. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/integrations/google.py +0 -0
  28. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/integrations/salesforce.py +0 -0
  29. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/integrations/slack.py +0 -0
  30. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/llm.py +0 -0
  31. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/pb.py +0 -0
  32. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/sdk.py +0 -0
  33. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/storage.py +0 -0
  34. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/webhooks.py +0 -0
  35. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera.egg-info/dependency_links.txt +0 -0
  36. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera.egg-info/requires.txt +1 -1
  37. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera.egg-info/top_level.txt +0 -0
  38. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/setup.cfg +0 -0
  39. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/tests/test_agents_live.py +0 -0
  40. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/tests/test_automations.py +0 -0
  41. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/tests/test_flags.py +0 -0
  42. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/tests/test_integrations_salesforce.py +0 -0
  43. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/tests/test_sdk.py +0 -0
  44. {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/tests/test_webhooks.py +0 -0
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lumera
3
- Version: 0.28.0.dev1
3
+ Version: 0.28.0.dev2
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
10
9
  Requires-Dist: simple-salesforce==1.12.6
11
10
  Requires-Dist: slack_sdk==3.35.0
12
11
  Provides-Extra: dev
@@ -29,6 +28,7 @@ Requires-Dist: openai-agents<1.0.0,>=0.6.5; extra == "full"
29
28
  Requires-Dist: openpyxl==3.1.5; extra == "full"
30
29
  Requires-Dist: pandas==2.3.0; extra == "full"
31
30
  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"
@@ -0,0 +1,175 @@
1
+ """
2
+ Lumera Agent SDK
3
+
4
+ This SDK provides helpers for automations running within the Lumera environment
5
+ to interact with the Lumera API and define dynamic user interfaces.
6
+ """
7
+
8
+ from importlib import import_module as _import_module
9
+ from importlib.metadata import PackageNotFoundError, version
10
+
11
+ try:
12
+ __version__ = version("lumera")
13
+ except PackageNotFoundError:
14
+ __version__ = "unknown" # Not installed (e.g. running from source)
15
+
16
+ # Define what ``from lumera import *`` imports. Keep this list stable: callers
17
+ # have historically imported these modules, helpers, types, and exceptions from
18
+ # the package root.
19
+ __all__ = [
20
+ # Authentication & utilities
21
+ "Credentials",
22
+ "get_access_token",
23
+ "get_credentials",
24
+ "get_google_access_token", # Kept for backwards compatibility
25
+ "list_integration_connections",
26
+ "list_integrations",
27
+ "log_timed",
28
+ # Feature flags (read the sandbox snapshot)
29
+ "feature_flag",
30
+ "feature_flags",
31
+ "is_feature_enabled",
32
+ # Collections (low-level API)
33
+ "list_collections",
34
+ "get_collection",
35
+ "create_collection",
36
+ "update_collection",
37
+ "delete_collection",
38
+ # Records (low-level API)
39
+ "list_records",
40
+ "search_records",
41
+ "get_record",
42
+ "get_record_by_external_id",
43
+ "create_record",
44
+ "update_record",
45
+ "upsert_record",
46
+ "delete_record",
47
+ # Other operations
48
+ "replay_hook",
49
+ "query_sql",
50
+ "run_automation",
51
+ "get_automation_run",
52
+ "update_automation_run",
53
+ "upload_lumera_file",
54
+ "save_to_lumera",
55
+ # Type definitions
56
+ "CollectionField",
57
+ "HookReplayResult",
58
+ "LumeraFile",
59
+ "LumeraFiles",
60
+ # Exceptions
61
+ "LumeraAPIError",
62
+ "RecordNotUniqueError",
63
+ "IntegrationNotEnabledForProjectError",
64
+ "LumeraError",
65
+ "ValidationError",
66
+ "UniqueConstraintError",
67
+ "RecordNotFoundError",
68
+ # High-level modules (use as lumera.pb, lumera.storage, etc.)
69
+ "agents",
70
+ "automations",
71
+ "email",
72
+ "pb",
73
+ "storage",
74
+ "llm",
75
+ "exceptions",
76
+ "webhooks",
77
+ "integrations",
78
+ ]
79
+
80
+ # Importing the package root used to import every SDK module, including all
81
+ # feature-specific integration clients. Resolve the same public API on first
82
+ # access so existing import statements retain their behavior without paying
83
+ # that cost when the corresponding feature is unused.
84
+ _LAZY_MODULES = {
85
+ "_utils": "._utils",
86
+ "agents": ".agents",
87
+ "automations": ".automations",
88
+ "email": ".email",
89
+ "exceptions": ".exceptions",
90
+ "files": ".files",
91
+ "flags": ".flags",
92
+ "integrations": ".integrations",
93
+ "llm": ".llm",
94
+ "pb": ".pb",
95
+ "sdk": ".sdk",
96
+ "storage": ".storage",
97
+ "webhooks": ".webhooks",
98
+ }
99
+
100
+ _LAZY_ATTRIBUTES = {
101
+ # ._utils
102
+ "Credentials": ("._utils", "Credentials"),
103
+ "IntegrationNotEnabledForProjectError": (
104
+ "._utils",
105
+ "IntegrationNotEnabledForProjectError",
106
+ ),
107
+ "LumeraAPIError": ("._utils", "LumeraAPIError"),
108
+ "RecordNotUniqueError": ("._utils", "RecordNotUniqueError"),
109
+ "get_access_token": ("._utils", "get_access_token"),
110
+ "get_credentials": ("._utils", "get_credentials"),
111
+ "get_google_access_token": ("._utils", "get_google_access_token"),
112
+ "list_integration_connections": ("._utils", "list_integration_connections"),
113
+ "list_integrations": ("._utils", "list_integrations"),
114
+ "log_timed": ("._utils", "log_timed"),
115
+ # .exceptions
116
+ "LumeraError": (".exceptions", "LumeraError"),
117
+ "RecordNotFoundError": (".exceptions", "RecordNotFoundError"),
118
+ "UniqueConstraintError": (".exceptions", "UniqueConstraintError"),
119
+ "ValidationError": (".exceptions", "ValidationError"),
120
+ # .files
121
+ "LumeraFile": (".files", "LumeraFile"),
122
+ "LumeraFiles": (".files", "LumeraFiles"),
123
+ # .flags
124
+ "feature_flag": (".flags", "feature_flag"),
125
+ "feature_flags": (".flags", "feature_flags"),
126
+ "is_feature_enabled": (".flags", "is_feature_enabled"),
127
+ # .sdk
128
+ "CollectionField": (".sdk", "CollectionField"),
129
+ "HookReplayResult": (".sdk", "HookReplayResult"),
130
+ "create_collection": (".sdk", "create_collection"),
131
+ "create_record": (".sdk", "create_record"),
132
+ "delete_collection": (".sdk", "delete_collection"),
133
+ "delete_record": (".sdk", "delete_record"),
134
+ "get_automation_run": (".sdk", "get_automation_run"),
135
+ "get_collection": (".sdk", "get_collection"),
136
+ "get_record": (".sdk", "get_record"),
137
+ "get_record_by_external_id": (".sdk", "get_record_by_external_id"),
138
+ "list_collections": (".sdk", "list_collections"),
139
+ "list_records": (".sdk", "list_records"),
140
+ "query_sql": (".sdk", "query_sql"),
141
+ "replay_hook": (".sdk", "replay_hook"),
142
+ "run_automation": (".sdk", "run_automation"),
143
+ "save_to_lumera": (".sdk", "save_to_lumera"),
144
+ "search_records": (".sdk", "search_records"),
145
+ "update_automation_run": (".sdk", "update_automation_run"),
146
+ "update_collection": (".sdk", "update_collection"),
147
+ "update_record": (".sdk", "update_record"),
148
+ "upload_lumera_file": (".sdk", "upload_lumera_file"),
149
+ "upsert_record": (".sdk", "upsert_record"),
150
+ }
151
+
152
+
153
+ def __getattr__(name: str) -> object:
154
+ """Load a root SDK export the first time it is requested."""
155
+
156
+ module_name = _LAZY_MODULES.get(name)
157
+ if module_name is not None:
158
+ value = _import_module(module_name, __name__)
159
+ globals()[name] = value
160
+ return value
161
+
162
+ attribute = _LAZY_ATTRIBUTES.get(name)
163
+ if attribute is not None:
164
+ module_name, attribute_name = attribute
165
+ value = getattr(_import_module(module_name, __name__), attribute_name)
166
+ globals()[name] = value
167
+ return value
168
+
169
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
170
+
171
+
172
+ def __dir__() -> list[str]:
173
+ """Include lazily available exports in interactive discovery."""
174
+
175
+ return sorted(set(globals()) | set(__all__) | set(_LAZY_MODULES))
@@ -3,9 +3,9 @@ Lumera NetSuite Integration
3
3
 
4
4
  Provides two NetSuite clients:
5
5
 
6
- * :class:`NetSuiteProxyClient` is recommended for new code. It routes requests
7
- through Lumera's integration proxy, so NetSuite credentials stay inside the
8
- Lumera backend.
6
+ * :class:`NetSuiteProxyClient` is recommended for new code. It routes SuiteQL,
7
+ REST records, and RESTlets through Lumera's integration proxy, so NetSuite
8
+ credentials stay inside the Lumera backend.
9
9
  * :class:`NetSuiteClient` preserves the existing direct-call behavior for
10
10
  backward compatibility. It downloads the NetSuite access token into the
11
11
  automation process and calls SuiteTalk directly.
@@ -99,6 +99,10 @@ def _base_url(account_id: str) -> str:
99
99
  return f"https://{account_id.lower()}.suitetalk.api.netsuite.com"
100
100
 
101
101
 
102
+ def _restlet_base_url(account_id: str) -> str:
103
+ return f"https://{account_id.lower()}.restlets.api.netsuite.com"
104
+
105
+
102
106
  def _fetch_account_id(api_name: str) -> str:
103
107
  """Resolve the NetSuite account ID from connection metadata (no secrets)."""
104
108
  cached = _account_id_cache.get(api_name)
@@ -417,6 +421,39 @@ class NetSuiteProxyClient:
417
421
  timeout=self._timeout,
418
422
  )
419
423
 
424
+ def call_restlet(
425
+ self,
426
+ script_id: str | int,
427
+ deployment_id: str | int,
428
+ *,
429
+ method: str = "POST",
430
+ body: dict[str, Any] | list[Any] | str | None = None,
431
+ headers: dict[str, str] | None = None,
432
+ params: dict[str, Any] | None = None,
433
+ ) -> requests.Response:
434
+ """Call a deployed RESTlet through Lumera's integration proxy.
435
+
436
+ RESTlet access must first be enabled for the NetSuite connection in
437
+ Lumera. The raw response is returned so callers can handle each
438
+ RESTlet's custom response format.
439
+ """
440
+ query: dict[str, Any] = {
441
+ "script": script_id,
442
+ "deploy": deployment_id,
443
+ }
444
+ for key, value in (params or {}).items():
445
+ if key not in query:
446
+ query[key] = value
447
+ url = f"{_restlet_base_url(self.account_id)}/app/site/hosting/restlet.nl?{urlencode(query)}"
448
+ return _proxy_request(
449
+ self._api_name,
450
+ method.upper(),
451
+ url,
452
+ body=body,
453
+ headers=headers,
454
+ timeout=self._timeout,
455
+ )
456
+
420
457
  # ── SuiteQL ──────────────────────────────────────────────────────────
421
458
 
422
459
  def suiteql(
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lumera
3
- Version: 0.28.0.dev1
3
+ Version: 0.28.0.dev2
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
10
9
  Requires-Dist: simple-salesforce==1.12.6
11
10
  Requires-Dist: slack_sdk==3.35.0
12
11
  Provides-Extra: dev
@@ -29,6 +28,7 @@ Requires-Dist: openai-agents<1.0.0,>=0.6.5; extra == "full"
29
28
  Requires-Dist: openpyxl==3.1.5; extra == "full"
30
29
  Requires-Dist: pandas==2.3.0; extra == "full"
31
30
  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"
@@ -19,11 +19,6 @@ lumera.egg-info/SOURCES.txt
19
19
  lumera.egg-info/dependency_links.txt
20
20
  lumera.egg-info/requires.txt
21
21
  lumera.egg-info/top_level.txt
22
- lumera/functions/__init__.py
23
- lumera/functions/__main__.py
24
- lumera/functions/_bundle.py
25
- lumera/functions/_core.py
26
- lumera/functions/_runner.py
27
22
  lumera/integrations/__init__.py
28
23
  lumera/integrations/bill.py
29
24
  lumera/integrations/google.py
@@ -33,8 +28,8 @@ lumera/integrations/slack.py
33
28
  tests/test_agents_live.py
34
29
  tests/test_automations.py
35
30
  tests/test_flags.py
36
- tests/test_functions.py
37
31
  tests/test_integrations_netsuite.py
38
32
  tests/test_integrations_salesforce.py
33
+ tests/test_lazy_imports.py
39
34
  tests/test_sdk.py
40
35
  tests/test_webhooks.py
@@ -1,13 +1,12 @@
1
1
  [project]
2
2
  name = "lumera"
3
- version = "0.28.0.dev1"
3
+ version = "0.28.0.dev2"
4
4
  description = "SDK for building on Lumera platform"
5
5
  requires-python = ">=3.11"
6
6
  dependencies = [
7
7
  "requests",
8
8
  "python-dotenv",
9
9
  "google-api-python-client==2.173.0",
10
- "pydantic>=2.12.3,<3.0.0",
11
10
  "simple-salesforce==1.12.6",
12
11
  "slack_sdk==3.35.0",
13
12
  ]
@@ -37,6 +36,7 @@ full = [
37
36
  "openpyxl==3.1.5",
38
37
  "pandas==2.3.0",
39
38
  "pdfplumber",
39
+ "pydantic>=2.12.3,<3.0.0",
40
40
  "PyGithub==2.6.1",
41
41
  "python-dotenv==1.1.0",
42
42
  "pyzmq",
@@ -177,6 +177,33 @@ def test_proxy_client_request_builds_url_with_params(monkeypatch: pytest.MonkeyP
177
177
  assert "body" not in payload
178
178
 
179
179
 
180
+ def test_proxy_client_call_restlet_uses_restlet_host(
181
+ monkeypatch: pytest.MonkeyPatch,
182
+ ) -> None:
183
+ session = MockSession(proxy_responses=[DummyResponse(json_data={"fileId": "42"})])
184
+ _install(monkeypatch, session)
185
+
186
+ ns = netsuite.NetSuiteProxyClient(api_name="netsuite_prod", timeout=120)
187
+ resp = ns.call_restlet(
188
+ 990,
189
+ 1,
190
+ body={"journalEntryId": "123"},
191
+ params={"mode": "attach"},
192
+ )
193
+
194
+ assert resp.json() == {"fileId": "42"}
195
+ call = session.post_calls[0]
196
+ payload = call["json"]
197
+ assert payload["api_name"] == "netsuite_prod"
198
+ assert payload["method"] == "POST"
199
+ assert payload["url"] == (
200
+ "https://12345_sb1.restlets.api.netsuite.com"
201
+ "/app/site/hosting/restlet.nl?script=990&deploy=1&mode=attach"
202
+ )
203
+ assert payload["body"] == {"journalEntryId": "123"}
204
+ assert payload["timeout_seconds"] == 120
205
+
206
+
180
207
  def test_proxy_timeout_is_bounded_and_requires_positive_value(
181
208
  monkeypatch: pytest.MonkeyPatch,
182
209
  ) -> None:
@@ -0,0 +1,230 @@
1
+ import json
2
+ import subprocess
3
+ import sys
4
+ from textwrap import dedent
5
+
6
+ EXPECTED_ROOT_EXPORTS = {
7
+ "CollectionField",
8
+ "Credentials",
9
+ "HookReplayResult",
10
+ "IntegrationNotEnabledForProjectError",
11
+ "LumeraAPIError",
12
+ "LumeraError",
13
+ "LumeraFile",
14
+ "LumeraFiles",
15
+ "RecordNotFoundError",
16
+ "RecordNotUniqueError",
17
+ "UniqueConstraintError",
18
+ "ValidationError",
19
+ "agents",
20
+ "automations",
21
+ "create_collection",
22
+ "create_record",
23
+ "delete_collection",
24
+ "delete_record",
25
+ "email",
26
+ "exceptions",
27
+ "feature_flag",
28
+ "feature_flags",
29
+ "get_access_token",
30
+ "get_automation_run",
31
+ "get_collection",
32
+ "get_credentials",
33
+ "get_google_access_token",
34
+ "get_record",
35
+ "get_record_by_external_id",
36
+ "integrations",
37
+ "is_feature_enabled",
38
+ "list_collections",
39
+ "list_integration_connections",
40
+ "list_integrations",
41
+ "list_records",
42
+ "llm",
43
+ "log_timed",
44
+ "pb",
45
+ "query_sql",
46
+ "replay_hook",
47
+ "run_automation",
48
+ "save_to_lumera",
49
+ "search_records",
50
+ "storage",
51
+ "update_automation_run",
52
+ "update_collection",
53
+ "update_record",
54
+ "upload_lumera_file",
55
+ "upsert_record",
56
+ "webhooks",
57
+ }
58
+
59
+
60
+ def _run_isolated(script: str) -> dict:
61
+ completed = subprocess.run(
62
+ [sys.executable, "-c", dedent(script)],
63
+ check=True,
64
+ capture_output=True,
65
+ text=True,
66
+ )
67
+ return json.loads(completed.stdout)
68
+
69
+
70
+ def test_public_root_export_contract_is_unchanged() -> None:
71
+ import lumera
72
+
73
+ assert set(lumera.__all__) == EXPECTED_ROOT_EXPORTS
74
+ assert EXPECTED_ROOT_EXPORTS <= set(dir(lumera))
75
+
76
+
77
+ def test_importing_package_root_does_not_load_feature_modules() -> None:
78
+ result = _run_isolated(
79
+ """
80
+ import json
81
+ import sys
82
+
83
+ import lumera
84
+
85
+ watched = [
86
+ "lumera.agents",
87
+ "lumera.automations",
88
+ "lumera.integrations",
89
+ "lumera.llm",
90
+ "lumera.pb",
91
+ "lumera.sdk",
92
+ "googleapiclient",
93
+ "simple_salesforce",
94
+ ]
95
+ print(json.dumps({
96
+ "loaded": [name for name in watched if name in sys.modules],
97
+ "has_version": bool(lumera.__version__),
98
+ }))
99
+ """
100
+ )
101
+
102
+ assert result == {"loaded": [], "has_version": True}
103
+
104
+
105
+ def test_accessing_root_helper_loads_only_its_defining_module() -> None:
106
+ result = _run_isolated(
107
+ """
108
+ import json
109
+ import sys
110
+
111
+ import lumera
112
+
113
+ helper = lumera.get_credentials
114
+ from lumera._utils import get_credentials
115
+
116
+ print(json.dumps({
117
+ "same_helper": helper is get_credentials,
118
+ "utils_loaded": "lumera._utils" in sys.modules,
119
+ "integrations_loaded": "lumera.integrations" in sys.modules,
120
+ "google_loaded": "googleapiclient" in sys.modules,
121
+ "salesforce_loaded": "simple_salesforce" in sys.modules,
122
+ }))
123
+ """
124
+ )
125
+
126
+ assert result == {
127
+ "same_helper": True,
128
+ "utils_loaded": True,
129
+ "integrations_loaded": False,
130
+ "google_loaded": False,
131
+ "salesforce_loaded": False,
132
+ }
133
+
134
+
135
+ def test_existing_root_exports_resolve_to_original_objects() -> None:
136
+ result = _run_isolated(
137
+ """
138
+ import json
139
+
140
+ import lumera
141
+ from lumera import (
142
+ LumeraFile,
143
+ RecordNotFoundError,
144
+ agents,
145
+ automations,
146
+ create_record,
147
+ feature_flag,
148
+ integrations,
149
+ )
150
+ from lumera.exceptions import RecordNotFoundError as DirectRecordNotFoundError
151
+ from lumera.files import LumeraFile as DirectLumeraFile
152
+ from lumera.flags import feature_flag as direct_feature_flag
153
+ from lumera.sdk import create_record as direct_create_record
154
+
155
+ print(json.dumps({
156
+ "agents": agents is lumera.agents,
157
+ "automations": automations is lumera.automations,
158
+ "integrations": integrations is lumera.integrations,
159
+ "create_record": create_record is direct_create_record,
160
+ "feature_flag": feature_flag is direct_feature_flag,
161
+ "file_type": LumeraFile is DirectLumeraFile,
162
+ "exception": RecordNotFoundError is DirectRecordNotFoundError,
163
+ }))
164
+ """
165
+ )
166
+
167
+ assert all(result.values())
168
+
169
+
170
+ def test_legacy_root_module_attributes_remain_available() -> None:
171
+ result = _run_isolated(
172
+ """
173
+ import importlib
174
+ import json
175
+
176
+ import lumera
177
+
178
+ module_names = ["_utils", "files", "flags", "sdk"]
179
+ print(json.dumps({
180
+ name: getattr(lumera, name) is importlib.import_module(f"lumera.{name}")
181
+ for name in module_names
182
+ }))
183
+ """
184
+ )
185
+
186
+ assert all(result.values())
187
+
188
+
189
+ def test_star_import_retains_the_existing_public_api() -> None:
190
+ result = _run_isolated(
191
+ """
192
+ import json
193
+
194
+ import lumera
195
+
196
+ expected = set(lumera.__all__)
197
+ namespace = {}
198
+ exec("from lumera import *", namespace)
199
+
200
+ print(json.dumps({
201
+ "missing": sorted(expected - namespace.keys()),
202
+ "unexpected": sorted(
203
+ name for name in namespace if not name.startswith("__") and name not in expected
204
+ ),
205
+ }))
206
+ """
207
+ )
208
+
209
+ assert result == {"missing": [], "unexpected": []}
210
+
211
+
212
+ def test_unknown_root_attribute_raises_standard_attribute_error() -> None:
213
+ result = _run_isolated(
214
+ """
215
+ import json
216
+
217
+ import lumera
218
+
219
+ try:
220
+ lumera.not_a_real_sdk_export
221
+ except AttributeError as exc:
222
+ error = str(exc)
223
+ else:
224
+ error = ""
225
+
226
+ print(json.dumps({"error": error}))
227
+ """
228
+ )
229
+
230
+ assert result == {"error": "module 'lumera' has no attribute 'not_a_real_sdk_export'"}