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.
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/PKG-INFO +2 -2
- lumera-0.28.0.dev2/lumera/__init__.py +175 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/integrations/netsuite.py +40 -3
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera.egg-info/PKG-INFO +2 -2
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera.egg-info/SOURCES.txt +1 -6
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/pyproject.toml +2 -2
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/tests/test_integrations_netsuite.py +27 -0
- lumera-0.28.0.dev2/tests/test_lazy_imports.py +230 -0
- lumera-0.28.0.dev1/lumera/__init__.py +0 -143
- lumera-0.28.0.dev1/lumera/functions/__init__.py +0 -55
- lumera-0.28.0.dev1/lumera/functions/__main__.py +0 -5
- lumera-0.28.0.dev1/lumera/functions/_bundle.py +0 -194
- lumera-0.28.0.dev1/lumera/functions/_core.py +0 -382
- lumera-0.28.0.dev1/lumera/functions/_runner.py +0 -163
- lumera-0.28.0.dev1/tests/test_functions.py +0 -528
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/_live.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/_utils.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/agents.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/automations.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/email.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/exceptions.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/files.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/flags.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/google.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/integrations/__init__.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/integrations/bill.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/integrations/google.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/integrations/salesforce.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/integrations/slack.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/llm.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/pb.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/sdk.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/storage.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera/webhooks.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera.egg-info/dependency_links.txt +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera.egg-info/requires.txt +1 -1
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/lumera.egg-info/top_level.txt +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/setup.cfg +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/tests/test_agents_live.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/tests/test_automations.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/tests/test_flags.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/tests/test_integrations_salesforce.py +0 -0
- {lumera-0.28.0.dev1 → lumera-0.28.0.dev2}/tests/test_sdk.py +0 -0
- {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.
|
|
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
|
|
7
|
-
through Lumera's integration proxy, so NetSuite
|
|
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.
|
|
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.
|
|
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'"}
|