licos-dev-sdk 0.6.4__tar.gz → 0.6.6__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.
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/PKG-INFO +2 -2
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/pyproject.toml +2 -2
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/model.py +4 -1
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/observability.py +3 -2
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_model.py +1 -1
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_observability.py +150 -150
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_request_identity.py +2 -2
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/.gitignore +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/DOCX.md +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/README.md +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/__init__.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/_usage_identity.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/_utils.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/archive.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/chart.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/data.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/diagram.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_common.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_edit.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_expectations.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_inspect.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_layout.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_package.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_preservation.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_structure.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_table.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_table_actions.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_read.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_table_export.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/image.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/langchain_reasoning.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/pdf_inspect.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/ppt_master_runtime.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/presentation.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/presentation_canvas.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/presentation_inspect.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/spreadsheet.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/spreadsheet_edit.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/spreadsheet_html.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/spreadsheet_inspect.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/web.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_document_read.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_document_spreadsheet.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_document_table_export.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_docx_inspect.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_docx_structure.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_docx_table_edit.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_html_docx_features.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_langchain_protocol.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_output_paths.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_ppt_master_runtime.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_presentation.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_spreadsheet_html_edit.py +0 -0
- {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_usage_identity.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: licos-dev-sdk
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.6
|
|
4
4
|
Summary: LICOS Dev SDK - file generation and model capability clients
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: beautifulsoup4>=4.12
|
|
7
7
|
Requires-Dist: docxtpl>=0.16
|
|
8
8
|
Requires-Dist: graphviz>=0.20
|
|
9
9
|
Requires-Dist: jinja2>=3.1
|
|
10
|
-
Requires-Dist: licos-platform-sdk>=0.7.
|
|
10
|
+
Requires-Dist: licos-platform-sdk>=0.7.6
|
|
11
11
|
Requires-Dist: matplotlib>=3.9
|
|
12
12
|
Requires-Dist: mistune>=3.0
|
|
13
13
|
Requires-Dist: openpyxl>=3.1
|
|
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "licos-dev-sdk"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.6"
|
|
8
8
|
description = "LICOS Dev SDK - file generation and model capability clients"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"licos-platform-sdk>=0.7.
|
|
12
|
+
"licos-platform-sdk>=0.7.6",
|
|
13
13
|
"weasyprint>=62.0",
|
|
14
14
|
"python-docx>=1.1",
|
|
15
15
|
"docxtpl>=0.16",
|
|
@@ -1229,6 +1229,7 @@ def _model_runtime(
|
|
|
1229
1229
|
token = runtime_user_token or resolve_user_token(
|
|
1230
1230
|
resolved_base_url,
|
|
1231
1231
|
owner_user_id,
|
|
1232
|
+
workspace_id=resolved_workspace_id,
|
|
1232
1233
|
)
|
|
1233
1234
|
owner_project_id = env("LICOS_PROJECT_ID") or env("AGENT_PROJECT_ID")
|
|
1234
1235
|
owner_project_name = env("LICOS_OWNER_PROJECT_NAME") or env("AGENT_OWNER_PROJECT_NAME")
|
|
@@ -1274,7 +1275,7 @@ def _model_runtime(
|
|
|
1274
1275
|
except ApiError as error:
|
|
1275
1276
|
if explicit_user_token or not should_refresh_user_token(error):
|
|
1276
1277
|
raise
|
|
1277
|
-
token = resolve_user_token(resolved_base_url, owner_user_id, force_refresh=True)
|
|
1278
|
+
token = resolve_user_token(resolved_base_url, owner_user_id, force_refresh=True, workspace_id=resolved_workspace_id)
|
|
1278
1279
|
current_user_id, current_user_name, current_org_id, current_org_name = _fetch_current_user_context(
|
|
1279
1280
|
resolved_base_url, token,
|
|
1280
1281
|
)
|
|
@@ -1332,6 +1333,7 @@ def _refresh_model_runtime(runtime: ModelRuntime) -> ModelRuntime:
|
|
|
1332
1333
|
runtime.base_url,
|
|
1333
1334
|
runtime.user_id,
|
|
1334
1335
|
force_refresh=True,
|
|
1336
|
+
workspace_id=runtime.workspace_id,
|
|
1335
1337
|
)
|
|
1336
1338
|
current_user_id, current_user_name, current_org_id, current_org_name = _fetch_current_user_context(
|
|
1337
1339
|
runtime.base_url,
|
|
@@ -1422,6 +1424,7 @@ def _credit_subject(
|
|
|
1422
1424
|
runtime.base_url,
|
|
1423
1425
|
owner_user_id,
|
|
1424
1426
|
force_refresh=force_refresh,
|
|
1427
|
+
workspace_id=runtime.workspace_id,
|
|
1425
1428
|
)
|
|
1426
1429
|
return subject_user_id, subject_org_id, token
|
|
1427
1430
|
|
|
@@ -31,6 +31,7 @@ class ObservabilityRuntime:
|
|
|
31
31
|
base_url: str
|
|
32
32
|
project_id: str
|
|
33
33
|
token: str
|
|
34
|
+
explicit_user_token: bool = False
|
|
34
35
|
workspace_id: str | None = None
|
|
35
36
|
user_id: str | None = None
|
|
36
37
|
project_type: str | None = None
|
|
@@ -289,7 +290,6 @@ def _observability_runtime(
|
|
|
289
290
|
) -> ObservabilityRuntime:
|
|
290
291
|
resolved_base_url = normalize_base_url(base_url) if base_url else platform_base_url()
|
|
291
292
|
owner_user_id = user_id or env("LICOS_USER_ID") or env("AGENT_USER_ID")
|
|
292
|
-
token = (user_token or "").strip() or resolve_user_token(resolved_base_url, owner_user_id)
|
|
293
293
|
resolved_project_id = project_id or env("LICOS_PROJECT_ID") or env("AGENT_PROJECT_ID")
|
|
294
294
|
if not resolved_project_id:
|
|
295
295
|
raise ConfigurationError("LICOS_PROJECT_ID or AGENT_PROJECT_ID is not configured")
|
|
@@ -300,6 +300,7 @@ def _observability_runtime(
|
|
|
300
300
|
or env("LICOS_OWNER_WORKSPACE_ID")
|
|
301
301
|
or env("AGENT_WORKSPACE_ID")
|
|
302
302
|
)
|
|
303
|
+
token = (user_token or "").strip() or resolve_user_token(resolved_base_url, owner_user_id, workspace_id=resolved_workspace_id)
|
|
303
304
|
resolved_project_type = (
|
|
304
305
|
project_type or env("LICOS_PROJECT_TYPE") or env("AGENT_PROJECT_TYPE")
|
|
305
306
|
)
|
|
@@ -325,7 +326,7 @@ def _observability_runtime(
|
|
|
325
326
|
|
|
326
327
|
|
|
327
328
|
def _refresh_runtime(runtime: ObservabilityRuntime) -> ObservabilityRuntime:
|
|
328
|
-
token = resolve_user_token(runtime.base_url, runtime.user_id, force_refresh=True)
|
|
329
|
+
token = resolve_user_token(runtime.base_url, runtime.user_id, force_refresh=True, workspace_id=runtime.workspace_id)
|
|
329
330
|
return replace(runtime, token=token)
|
|
330
331
|
|
|
331
332
|
|
|
@@ -521,7 +521,7 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
521
521
|
|
|
522
522
|
self.assertEqual(result.text, "hello")
|
|
523
523
|
self.assertEqual(captured["exchange_headers"]["Authorization"], "Bearer ai-agent-token")
|
|
524
|
-
self.assertEqual(captured["exchange_body"], {"userId": "user-1"})
|
|
524
|
+
self.assertEqual(captured["exchange_body"], {"userId": "user-1", "workspaceId": "workspace-1"})
|
|
525
525
|
self.assertEqual(captured["catalog_headers"]["Authorization"], "Bearer user-token")
|
|
526
526
|
self.assertEqual(captured["detail_headers"]["Authorization"], "Bearer user-token")
|
|
527
527
|
self.assertEqual(captured["chat_headers"]["Authorization"], "Bearer user-token")
|
|
@@ -1,150 +1,150 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import sys
|
|
6
|
-
import unittest
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
9
|
-
from unittest import mock
|
|
10
|
-
from urllib import error as urlerror
|
|
11
|
-
|
|
12
|
-
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
13
|
-
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "licos-platform-sdk" / "src"))
|
|
14
|
-
|
|
15
|
-
from licos_dev_sdk import observability
|
|
16
|
-
from licos_platform_sdk import _runtime
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class _FakeResponse:
|
|
20
|
-
status = 200
|
|
21
|
-
|
|
22
|
-
def __init__(self, payload: dict[str, Any]) -> None:
|
|
23
|
-
self._payload = payload
|
|
24
|
-
|
|
25
|
-
def __enter__(self) -> "_FakeResponse":
|
|
26
|
-
return self
|
|
27
|
-
|
|
28
|
-
def __exit__(self, *_args: Any) -> None:
|
|
29
|
-
return None
|
|
30
|
-
|
|
31
|
-
def read(self) -> bytes:
|
|
32
|
-
return json.dumps(self._payload).encode("utf-8")
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class _FakeErrorBody:
|
|
36
|
-
def __init__(self, payload: dict[str, Any]) -> None:
|
|
37
|
-
self._payload = payload
|
|
38
|
-
|
|
39
|
-
def read(self) -> bytes:
|
|
40
|
-
return json.dumps(self._payload).encode("utf-8")
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class ObservabilitySdkTests(unittest.TestCase):
|
|
44
|
-
def setUp(self) -> None:
|
|
45
|
-
self.env = mock.patch.dict(
|
|
46
|
-
os.environ,
|
|
47
|
-
{
|
|
48
|
-
"LICOS_PLATFORM_API_BASE_URL": "http://platform.example/api/v1",
|
|
49
|
-
"AGENT_USER_ID": "user-1",
|
|
50
|
-
"AGENT_WORKSPACE_ID": "workspace-1",
|
|
51
|
-
"AGENT_PROJECT_ID": "project-1",
|
|
52
|
-
"AGENT_PROJECT_TYPE": "AGENT",
|
|
53
|
-
"LICOS_AI_AGENT_TOKEN": "ai-agent-token",
|
|
54
|
-
"LICOS_DEPLOYMENT_VERSION": "v1",
|
|
55
|
-
"LICOS_DEPLOYMENT_HASH": "hash-1",
|
|
56
|
-
},
|
|
57
|
-
clear=True,
|
|
58
|
-
)
|
|
59
|
-
self.env.start()
|
|
60
|
-
self.addCleanup(self.env.stop)
|
|
61
|
-
_runtime._clear_token_cache_for_tests()
|
|
62
|
-
|
|
63
|
-
def test_log_uses_runtime_context_and_owner_token(self) -> None:
|
|
64
|
-
captured: dict[str, Any] = {}
|
|
65
|
-
|
|
66
|
-
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
67
|
-
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
68
|
-
captured["exchange_headers"] = dict(req.header_items())
|
|
69
|
-
captured["exchange_body"] = json.loads(req.data.decode("utf-8"))
|
|
70
|
-
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
71
|
-
if req.full_url == "http://platform.example/api/v1/studio/observability/logs?projectId=project-1":
|
|
72
|
-
captured["log_headers"] = dict(req.header_items())
|
|
73
|
-
captured["log_body"] = json.loads(req.data.decode("utf-8"))
|
|
74
|
-
return _FakeResponse({"code": 0, "success": True, "data": {"id": 1}})
|
|
75
|
-
raise AssertionError(req.full_url)
|
|
76
|
-
|
|
77
|
-
with mock.patch.object(observability.request, "urlopen", fake_urlopen):
|
|
78
|
-
result = observability.log_info("started", log_time="2026-05-22T10:00:00+08:00")
|
|
79
|
-
|
|
80
|
-
self.assertEqual(result, {"id": 1})
|
|
81
|
-
self.assertEqual(captured["exchange_headers"]["Authorization"], "Bearer ai-agent-token")
|
|
82
|
-
self.assertEqual(captured["exchange_body"], {"userId": "user-1"})
|
|
83
|
-
self.assertEqual(captured["log_headers"]["Authorization"], "Bearer user-token")
|
|
84
|
-
log_headers = {key.lower(): value for key, value in captured["log_headers"].items()}
|
|
85
|
-
self.assertEqual(log_headers["x-workspace-id"], "workspace-1")
|
|
86
|
-
self.assertEqual(captured["log_body"]["deploymentVersion"], "v1")
|
|
87
|
-
self.assertEqual(captured["log_body"]["level"], "info")
|
|
88
|
-
self.assertEqual(captured["log_body"]["logContent"], "started")
|
|
89
|
-
|
|
90
|
-
def test_trace_metric_and_error_payloads(self) -> None:
|
|
91
|
-
calls: list[tuple[str, dict[str, Any]]] = []
|
|
92
|
-
|
|
93
|
-
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
94
|
-
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
95
|
-
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
96
|
-
calls.append((req.full_url, json.loads(req.data.decode("utf-8"))))
|
|
97
|
-
return _FakeResponse({"code": 0, "success": True, "data": {"ok": True}})
|
|
98
|
-
|
|
99
|
-
with mock.patch.object(observability.request, "urlopen", fake_urlopen):
|
|
100
|
-
client = observability.ObservabilityClient()
|
|
101
|
-
client.record_trace(trace_id="trace-1", input={"q": "hi"}, output="ok", input_tokens=3, output_tokens=4, latency_seconds=1.2)
|
|
102
|
-
client.record_metric(route="/stream_run", method="post", latency_ms=640, status_code=500)
|
|
103
|
-
client.record_error(route="/stream_run", method="post", error_code="MODEL_TIMEOUT", error_message="timeout", trace_id="trace-1")
|
|
104
|
-
|
|
105
|
-
self.assertEqual(calls[0][0], "http://platform.example/api/v1/studio/observability/traces?projectId=project-1")
|
|
106
|
-
self.assertEqual(calls[0][1]["traceId"], "trace-1")
|
|
107
|
-
self.assertEqual(calls[0][1]["hashVersion"], "hash-1")
|
|
108
|
-
self.assertEqual(calls[0][1]["input"], '{"q": "hi"}')
|
|
109
|
-
self.assertEqual(calls[0][1]["inputTokens"], 3)
|
|
110
|
-
|
|
111
|
-
self.assertEqual(calls[1][0], "http://platform.example/api/v1/studio/observability/analysis/metrics?projectId=project-1")
|
|
112
|
-
self.assertEqual(calls[1][1]["workspaceId"], "workspace-1")
|
|
113
|
-
self.assertEqual(calls[1][1]["projectType"], "AGENT")
|
|
114
|
-
self.assertEqual(calls[1][1]["method"], "POST")
|
|
115
|
-
self.assertEqual(calls[1][1]["errorCount"], 1)
|
|
116
|
-
self.assertEqual(calls[1][1]["status5xxCount"], 1)
|
|
117
|
-
self.assertEqual(calls[1][1]["latencyBuckets"], {"500+": 1})
|
|
118
|
-
|
|
119
|
-
self.assertEqual(calls[2][0], "http://platform.example/api/v1/studio/observability/analysis/errors?projectId=project-1")
|
|
120
|
-
self.assertEqual(calls[2][1]["errorCode"], "MODEL_TIMEOUT")
|
|
121
|
-
self.assertEqual(calls[2][1]["traceId"], "trace-1")
|
|
122
|
-
|
|
123
|
-
def test_refreshes_owner_token_once_after_unauthorized(self) -> None:
|
|
124
|
-
tokens = iter(["old-token", "new-token"])
|
|
125
|
-
log_tokens: list[str] = []
|
|
126
|
-
|
|
127
|
-
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
128
|
-
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
129
|
-
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": next(tokens)}})
|
|
130
|
-
if req.full_url == "http://platform.example/api/v1/studio/observability/logs?projectId=project-1":
|
|
131
|
-
log_tokens.append(dict(req.header_items())["Authorization"])
|
|
132
|
-
if len(log_tokens) == 1:
|
|
133
|
-
raise urlerror.HTTPError(
|
|
134
|
-
req.full_url,
|
|
135
|
-
401,
|
|
136
|
-
"Unauthorized",
|
|
137
|
-
hdrs=None,
|
|
138
|
-
fp=_FakeErrorBody({"code": 10002, "message": "token invalid or expired", "success": False}),
|
|
139
|
-
)
|
|
140
|
-
return _FakeResponse({"code": 0, "success": True, "data": {"id": 1}})
|
|
141
|
-
raise AssertionError(req.full_url)
|
|
142
|
-
|
|
143
|
-
with mock.patch.object(observability.request, "urlopen", fake_urlopen):
|
|
144
|
-
observability.log_error("failed")
|
|
145
|
-
|
|
146
|
-
self.assertEqual(log_tokens, ["Bearer old-token", "Bearer new-token"])
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if __name__ == "__main__":
|
|
150
|
-
unittest.main()
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
from unittest import mock
|
|
10
|
+
from urllib import error as urlerror
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "licos-platform-sdk" / "src"))
|
|
14
|
+
|
|
15
|
+
from licos_dev_sdk import observability
|
|
16
|
+
from licos_platform_sdk import _runtime
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _FakeResponse:
|
|
20
|
+
status = 200
|
|
21
|
+
|
|
22
|
+
def __init__(self, payload: dict[str, Any]) -> None:
|
|
23
|
+
self._payload = payload
|
|
24
|
+
|
|
25
|
+
def __enter__(self) -> "_FakeResponse":
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def __exit__(self, *_args: Any) -> None:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
def read(self) -> bytes:
|
|
32
|
+
return json.dumps(self._payload).encode("utf-8")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _FakeErrorBody:
|
|
36
|
+
def __init__(self, payload: dict[str, Any]) -> None:
|
|
37
|
+
self._payload = payload
|
|
38
|
+
|
|
39
|
+
def read(self) -> bytes:
|
|
40
|
+
return json.dumps(self._payload).encode("utf-8")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ObservabilitySdkTests(unittest.TestCase):
|
|
44
|
+
def setUp(self) -> None:
|
|
45
|
+
self.env = mock.patch.dict(
|
|
46
|
+
os.environ,
|
|
47
|
+
{
|
|
48
|
+
"LICOS_PLATFORM_API_BASE_URL": "http://platform.example/api/v1",
|
|
49
|
+
"AGENT_USER_ID": "user-1",
|
|
50
|
+
"AGENT_WORKSPACE_ID": "workspace-1",
|
|
51
|
+
"AGENT_PROJECT_ID": "project-1",
|
|
52
|
+
"AGENT_PROJECT_TYPE": "AGENT",
|
|
53
|
+
"LICOS_AI_AGENT_TOKEN": "ai-agent-token",
|
|
54
|
+
"LICOS_DEPLOYMENT_VERSION": "v1",
|
|
55
|
+
"LICOS_DEPLOYMENT_HASH": "hash-1",
|
|
56
|
+
},
|
|
57
|
+
clear=True,
|
|
58
|
+
)
|
|
59
|
+
self.env.start()
|
|
60
|
+
self.addCleanup(self.env.stop)
|
|
61
|
+
_runtime._clear_token_cache_for_tests()
|
|
62
|
+
|
|
63
|
+
def test_log_uses_runtime_context_and_owner_token(self) -> None:
|
|
64
|
+
captured: dict[str, Any] = {}
|
|
65
|
+
|
|
66
|
+
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
67
|
+
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
68
|
+
captured["exchange_headers"] = dict(req.header_items())
|
|
69
|
+
captured["exchange_body"] = json.loads(req.data.decode("utf-8"))
|
|
70
|
+
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
71
|
+
if req.full_url == "http://platform.example/api/v1/studio/observability/logs?projectId=project-1":
|
|
72
|
+
captured["log_headers"] = dict(req.header_items())
|
|
73
|
+
captured["log_body"] = json.loads(req.data.decode("utf-8"))
|
|
74
|
+
return _FakeResponse({"code": 0, "success": True, "data": {"id": 1}})
|
|
75
|
+
raise AssertionError(req.full_url)
|
|
76
|
+
|
|
77
|
+
with mock.patch.object(observability.request, "urlopen", fake_urlopen):
|
|
78
|
+
result = observability.log_info("started", log_time="2026-05-22T10:00:00+08:00")
|
|
79
|
+
|
|
80
|
+
self.assertEqual(result, {"id": 1})
|
|
81
|
+
self.assertEqual(captured["exchange_headers"]["Authorization"], "Bearer ai-agent-token")
|
|
82
|
+
self.assertEqual(captured["exchange_body"], {"userId": "user-1", "workspaceId": "workspace-1"})
|
|
83
|
+
self.assertEqual(captured["log_headers"]["Authorization"], "Bearer user-token")
|
|
84
|
+
log_headers = {key.lower(): value for key, value in captured["log_headers"].items()}
|
|
85
|
+
self.assertEqual(log_headers["x-workspace-id"], "workspace-1")
|
|
86
|
+
self.assertEqual(captured["log_body"]["deploymentVersion"], "v1")
|
|
87
|
+
self.assertEqual(captured["log_body"]["level"], "info")
|
|
88
|
+
self.assertEqual(captured["log_body"]["logContent"], "started")
|
|
89
|
+
|
|
90
|
+
def test_trace_metric_and_error_payloads(self) -> None:
|
|
91
|
+
calls: list[tuple[str, dict[str, Any]]] = []
|
|
92
|
+
|
|
93
|
+
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
94
|
+
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
95
|
+
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
96
|
+
calls.append((req.full_url, json.loads(req.data.decode("utf-8"))))
|
|
97
|
+
return _FakeResponse({"code": 0, "success": True, "data": {"ok": True}})
|
|
98
|
+
|
|
99
|
+
with mock.patch.object(observability.request, "urlopen", fake_urlopen):
|
|
100
|
+
client = observability.ObservabilityClient()
|
|
101
|
+
client.record_trace(trace_id="trace-1", input={"q": "hi"}, output="ok", input_tokens=3, output_tokens=4, latency_seconds=1.2)
|
|
102
|
+
client.record_metric(route="/stream_run", method="post", latency_ms=640, status_code=500)
|
|
103
|
+
client.record_error(route="/stream_run", method="post", error_code="MODEL_TIMEOUT", error_message="timeout", trace_id="trace-1")
|
|
104
|
+
|
|
105
|
+
self.assertEqual(calls[0][0], "http://platform.example/api/v1/studio/observability/traces?projectId=project-1")
|
|
106
|
+
self.assertEqual(calls[0][1]["traceId"], "trace-1")
|
|
107
|
+
self.assertEqual(calls[0][1]["hashVersion"], "hash-1")
|
|
108
|
+
self.assertEqual(calls[0][1]["input"], '{"q": "hi"}')
|
|
109
|
+
self.assertEqual(calls[0][1]["inputTokens"], 3)
|
|
110
|
+
|
|
111
|
+
self.assertEqual(calls[1][0], "http://platform.example/api/v1/studio/observability/analysis/metrics?projectId=project-1")
|
|
112
|
+
self.assertEqual(calls[1][1]["workspaceId"], "workspace-1")
|
|
113
|
+
self.assertEqual(calls[1][1]["projectType"], "AGENT")
|
|
114
|
+
self.assertEqual(calls[1][1]["method"], "POST")
|
|
115
|
+
self.assertEqual(calls[1][1]["errorCount"], 1)
|
|
116
|
+
self.assertEqual(calls[1][1]["status5xxCount"], 1)
|
|
117
|
+
self.assertEqual(calls[1][1]["latencyBuckets"], {"500+": 1})
|
|
118
|
+
|
|
119
|
+
self.assertEqual(calls[2][0], "http://platform.example/api/v1/studio/observability/analysis/errors?projectId=project-1")
|
|
120
|
+
self.assertEqual(calls[2][1]["errorCode"], "MODEL_TIMEOUT")
|
|
121
|
+
self.assertEqual(calls[2][1]["traceId"], "trace-1")
|
|
122
|
+
|
|
123
|
+
def test_refreshes_owner_token_once_after_unauthorized(self) -> None:
|
|
124
|
+
tokens = iter(["old-token", "new-token"])
|
|
125
|
+
log_tokens: list[str] = []
|
|
126
|
+
|
|
127
|
+
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
128
|
+
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
129
|
+
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": next(tokens)}})
|
|
130
|
+
if req.full_url == "http://platform.example/api/v1/studio/observability/logs?projectId=project-1":
|
|
131
|
+
log_tokens.append(dict(req.header_items())["Authorization"])
|
|
132
|
+
if len(log_tokens) == 1:
|
|
133
|
+
raise urlerror.HTTPError(
|
|
134
|
+
req.full_url,
|
|
135
|
+
401,
|
|
136
|
+
"Unauthorized",
|
|
137
|
+
hdrs=None,
|
|
138
|
+
fp=_FakeErrorBody({"code": 10002, "message": "token invalid or expired", "success": False}),
|
|
139
|
+
)
|
|
140
|
+
return _FakeResponse({"code": 0, "success": True, "data": {"id": 1}})
|
|
141
|
+
raise AssertionError(req.full_url)
|
|
142
|
+
|
|
143
|
+
with mock.patch.object(observability.request, "urlopen", fake_urlopen):
|
|
144
|
+
observability.log_error("failed")
|
|
145
|
+
|
|
146
|
+
self.assertEqual(log_tokens, ["Bearer old-token", "Bearer new-token"])
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
unittest.main()
|
|
@@ -17,8 +17,8 @@ class ModelIdentityTests(TestCase):
|
|
|
17
17
|
self.assertEqual(runtime.token, "renewed")
|
|
18
18
|
self.assertEqual(runtime.current_user_id, "owner")
|
|
19
19
|
self.assertEqual(exchange.call_args_list, [
|
|
20
|
-
mock.call("https://platform.example", "owner"),
|
|
21
|
-
mock.call("https://platform.example", "owner", force_refresh=True),
|
|
20
|
+
mock.call("https://platform.example", "owner", workspace_id=None),
|
|
21
|
+
mock.call("https://platform.example", "owner", force_refresh=True, workspace_id=None),
|
|
22
22
|
])
|
|
23
23
|
|
|
24
24
|
def test_business_authentication_error_is_not_a_user_profile(self):
|
|
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
|
{licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_table_actions.py
RENAMED
|
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
|