lumera 0.27.1__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.27.1 → lumera-0.28.0.dev2}/PKG-INFO +1 -1
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/integrations/netsuite.py +40 -3
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera.egg-info/PKG-INFO +1 -1
- {lumera-0.27.1 → lumera-0.28.0.dev2}/pyproject.toml +1 -1
- {lumera-0.27.1 → lumera-0.28.0.dev2}/tests/test_integrations_netsuite.py +27 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/__init__.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/_live.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/_utils.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/agents.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/automations.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/email.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/exceptions.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/files.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/flags.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/google.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/integrations/__init__.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/integrations/bill.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/integrations/google.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/integrations/salesforce.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/integrations/slack.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/llm.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/pb.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/sdk.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/storage.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera/webhooks.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera.egg-info/SOURCES.txt +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera.egg-info/dependency_links.txt +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera.egg-info/requires.txt +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/lumera.egg-info/top_level.txt +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/setup.cfg +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/tests/test_agents_live.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/tests/test_automations.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/tests/test_flags.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/tests/test_integrations_salesforce.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/tests/test_lazy_imports.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/tests/test_sdk.py +0 -0
- {lumera-0.27.1 → lumera-0.28.0.dev2}/tests/test_webhooks.py +0 -0
|
@@ -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(
|
|
@@ -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:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|