lmnr 0.4.8__py3-none-any.whl → 0.4.9__py3-none-any.whl

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.
@@ -1,111 +1,111 @@
1
- """Unit tests configuration module."""
1
+ # """Unit tests configuration module."""
2
2
 
3
- import os
4
- import pytest
5
- from lmnr.traceloop_sdk import Traceloop
6
- from lmnr.traceloop_sdk.instruments import Instruments
7
- from lmnr.traceloop_sdk.tracing.tracing import TracerWrapper
8
- from opentelemetry.sdk.trace.export import SimpleSpanProcessor
9
- from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
3
+ # import os
4
+ # import pytest
5
+ # from lmnr.traceloop_sdk import Traceloop
6
+ # from lmnr.traceloop_sdk.instruments import Instruments
7
+ # from lmnr.traceloop_sdk.tracing.tracing import TracerWrapper
8
+ # from opentelemetry.sdk.trace.export import SimpleSpanProcessor
9
+ # from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
10
10
 
11
- # pytest_plugins = []
11
+ # # pytest_plugins = []
12
12
 
13
13
 
14
- @pytest.fixture(scope="session")
15
- def exporter():
16
- exporter = InMemorySpanExporter()
17
- Traceloop.init(
18
- app_name="test",
19
- resource_attributes={"something": "yes"},
20
- disable_batch=True,
21
- exporter=exporter,
22
- )
23
- return exporter
14
+ # @pytest.fixture(scope="session")
15
+ # def exporter():
16
+ # exporter = InMemorySpanExporter()
17
+ # Traceloop.init(
18
+ # app_name="test",
19
+ # resource_attributes={"something": "yes"},
20
+ # disable_batch=True,
21
+ # exporter=exporter,
22
+ # )
23
+ # return exporter
24
24
 
25
25
 
26
- @pytest.fixture(autouse=True)
27
- def clear_exporter(exporter):
28
- exporter.clear()
26
+ # @pytest.fixture(autouse=True)
27
+ # def clear_exporter(exporter):
28
+ # exporter.clear()
29
29
 
30
30
 
31
- @pytest.fixture(autouse=True)
32
- def environment():
33
- if "OPENAI_API_KEY" not in os.environ:
34
- os.environ["OPENAI_API_KEY"] = "test_api_key"
31
+ # @pytest.fixture(autouse=True)
32
+ # def environment():
33
+ # if "OPENAI_API_KEY" not in os.environ:
34
+ # os.environ["OPENAI_API_KEY"] = "test_api_key"
35
35
 
36
36
 
37
- @pytest.fixture(scope="module")
38
- def vcr_config():
39
- return {
40
- "filter_headers": ["authorization"],
41
- "ignore_hosts": ["openaipublic.blob.core.windows.net"],
42
- }
37
+ # @pytest.fixture(scope="module")
38
+ # def vcr_config():
39
+ # return {
40
+ # "filter_headers": ["authorization"],
41
+ # "ignore_hosts": ["openaipublic.blob.core.windows.net"],
42
+ # }
43
43
 
44
44
 
45
- @pytest.fixture
46
- def exporter_with_custom_span_processor():
47
- # Clear singleton if existed
48
- if hasattr(TracerWrapper, "instance"):
49
- _trace_wrapper_instance = TracerWrapper.instance
50
- del TracerWrapper.instance
45
+ # @pytest.fixture
46
+ # def exporter_with_custom_span_processor():
47
+ # # Clear singleton if existed
48
+ # if hasattr(TracerWrapper, "instance"):
49
+ # _trace_wrapper_instance = TracerWrapper.instance
50
+ # del TracerWrapper.instance
51
51
 
52
- class CustomSpanProcessor(SimpleSpanProcessor):
53
- def on_start(self, span, parent_context=None):
54
- span.set_attribute("custom_span", "yes")
52
+ # class CustomSpanProcessor(SimpleSpanProcessor):
53
+ # def on_start(self, span, parent_context=None):
54
+ # span.set_attribute("custom_span", "yes")
55
55
 
56
- exporter = InMemorySpanExporter()
57
- Traceloop.init(
58
- exporter=exporter,
59
- processor=CustomSpanProcessor(exporter),
60
- )
56
+ # exporter = InMemorySpanExporter()
57
+ # Traceloop.init(
58
+ # exporter=exporter,
59
+ # processor=CustomSpanProcessor(exporter),
60
+ # )
61
61
 
62
- yield exporter
62
+ # yield exporter
63
63
 
64
- # Restore singleton if any
65
- if _trace_wrapper_instance:
66
- TracerWrapper.instance = _trace_wrapper_instance
64
+ # # Restore singleton if any
65
+ # if _trace_wrapper_instance:
66
+ # TracerWrapper.instance = _trace_wrapper_instance
67
67
 
68
68
 
69
- @pytest.fixture
70
- def exporter_with_custom_instrumentations():
71
- # Clear singleton if existed
72
- if hasattr(TracerWrapper, "instance"):
73
- _trace_wrapper_instance = TracerWrapper.instance
74
- del TracerWrapper.instance
69
+ # @pytest.fixture
70
+ # def exporter_with_custom_instrumentations():
71
+ # # Clear singleton if existed
72
+ # if hasattr(TracerWrapper, "instance"):
73
+ # _trace_wrapper_instance = TracerWrapper.instance
74
+ # del TracerWrapper.instance
75
75
 
76
- exporter = InMemorySpanExporter()
77
- Traceloop.init(
78
- exporter=exporter,
79
- disable_batch=True,
80
- instruments=[i for i in Instruments],
81
- )
76
+ # exporter = InMemorySpanExporter()
77
+ # Traceloop.init(
78
+ # exporter=exporter,
79
+ # disable_batch=True,
80
+ # instruments=[i for i in Instruments],
81
+ # )
82
82
 
83
- yield exporter
83
+ # yield exporter
84
84
 
85
- # Restore singleton if any
86
- if _trace_wrapper_instance:
87
- TracerWrapper.instance = _trace_wrapper_instance
85
+ # # Restore singleton if any
86
+ # if _trace_wrapper_instance:
87
+ # TracerWrapper.instance = _trace_wrapper_instance
88
88
 
89
89
 
90
- @pytest.fixture
91
- def exporter_with_no_metrics():
92
- # Clear singleton if existed
93
- if hasattr(TracerWrapper, "instance"):
94
- _trace_wrapper_instance = TracerWrapper.instance
95
- del TracerWrapper.instance
90
+ # @pytest.fixture
91
+ # def exporter_with_no_metrics():
92
+ # # Clear singleton if existed
93
+ # if hasattr(TracerWrapper, "instance"):
94
+ # _trace_wrapper_instance = TracerWrapper.instance
95
+ # del TracerWrapper.instance
96
96
 
97
- os.environ["TRACELOOP_METRICS_ENABLED"] = "false"
97
+ # os.environ["TRACELOOP_METRICS_ENABLED"] = "false"
98
98
 
99
- exporter = InMemorySpanExporter()
99
+ # exporter = InMemorySpanExporter()
100
100
 
101
- Traceloop.init(
102
- exporter=exporter,
103
- disable_batch=True,
104
- )
101
+ # Traceloop.init(
102
+ # exporter=exporter,
103
+ # disable_batch=True,
104
+ # )
105
105
 
106
- yield exporter
106
+ # yield exporter
107
107
 
108
- # Restore singleton if any
109
- if _trace_wrapper_instance:
110
- TracerWrapper.instance = _trace_wrapper_instance
111
- os.environ["TRACELOOP_METRICS_ENABLED"] = "true"
108
+ # # Restore singleton if any
109
+ # if _trace_wrapper_instance:
110
+ # TracerWrapper.instance = _trace_wrapper_instance
111
+ # os.environ["TRACELOOP_METRICS_ENABLED"] = "true"
@@ -1,229 +1,229 @@
1
- import pytest
2
- from langchain_openai import ChatOpenAI
3
- from langchain.prompts import ChatPromptTemplate
4
- from opentelemetry.semconv_ai import SpanAttributes
5
- from lmnr.traceloop_sdk import Traceloop
6
- from lmnr.traceloop_sdk.decorators import task, workflow
7
-
8
-
9
- def test_association_properties(exporter):
10
- @workflow(name="test_workflow")
11
- def test_workflow():
12
- return test_task()
13
-
14
- @task(name="test_task")
15
- def test_task():
16
- return
17
-
18
- Traceloop.set_association_properties({"user_id": 1, "user_name": "John Doe"})
19
- test_workflow()
20
-
21
- spans = exporter.get_finished_spans()
22
- assert [span.name for span in spans] == [
23
- "test_task.task",
24
- "test_workflow.workflow",
25
- ]
26
-
27
- some_task_span = spans[0]
28
- some_workflow_span = spans[1]
29
- assert (
30
- some_workflow_span.attributes[
31
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id"
32
- ]
33
- == 1
34
- )
35
- assert (
36
- some_workflow_span.attributes[
37
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_name"
38
- ]
39
- == "John Doe"
40
- )
41
- assert (
42
- some_task_span.attributes[
43
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id"
44
- ]
45
- == 1
46
- )
47
- assert (
48
- some_task_span.attributes[
49
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_name"
50
- ]
51
- == "John Doe"
52
- )
53
-
54
-
55
- def test_association_properties_within_workflow(exporter):
56
- @workflow(name="test_workflow_within")
57
- def test_workflow():
58
- Traceloop.set_association_properties({"session_id": 15})
59
- return
60
-
61
- test_workflow()
62
-
63
- spans = exporter.get_finished_spans()
64
- assert [span.name for span in spans] == [
65
- "test_workflow_within.workflow",
66
- ]
67
-
68
- some_workflow_span = spans[0]
69
- assert (
70
- some_workflow_span.attributes[
71
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id"
72
- ]
73
- == 15
74
- )
75
-
76
-
77
- @pytest.mark.vcr
78
- def test_langchain_association_properties(exporter):
79
- prompt = ChatPromptTemplate.from_messages(
80
- [("system", "You are helpful assistant"), ("user", "{input}")]
81
- )
82
- model = ChatOpenAI(model="gpt-3.5-turbo")
83
-
84
- chain = prompt | model
85
- chain.invoke(
86
- {"input": "tell me a short joke"},
87
- {"metadata": {"user_id": "1234", "session_id": 456}},
88
- )
89
-
90
- spans = exporter.get_finished_spans()
91
-
92
- assert [
93
- "ChatPromptTemplate.task",
94
- "ChatOpenAI.chat",
95
- "RunnableSequence.workflow",
96
- ] == [span.name for span in spans], [span.name for span in spans]
97
-
98
- workflow_span = next(
99
- span for span in spans if span.name == "RunnableSequence.workflow"
100
- )
101
- prompt_span = next(span for span in spans if span.name == "ChatPromptTemplate.task")
102
- chat_span = next(span for span in spans if span.name == "ChatOpenAI.chat")
103
-
104
- assert (
105
- workflow_span.attributes[
106
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id"
107
- ]
108
- == "1234"
109
- )
110
- assert (
111
- workflow_span.attributes[
112
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id"
113
- ]
114
- == 456
115
- )
116
- assert (
117
- chat_span.attributes[
118
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id"
119
- ]
120
- == "1234"
121
- )
122
- assert (
123
- chat_span.attributes[
124
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id"
125
- ]
126
- == 456
127
- )
128
- assert (
129
- prompt_span.attributes[
130
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id"
131
- ]
132
- == "1234"
133
- )
134
- assert (
135
- prompt_span.attributes[
136
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id"
137
- ]
138
- == 456
139
- )
140
-
141
-
142
- @pytest.mark.vcr
143
- def test_langchain_and_external_association_properties(exporter):
144
- @workflow(name="test_workflow_external")
145
- def test_workflow_external():
146
- Traceloop.set_association_properties({"workspace_id": "789"})
147
-
148
- prompt = ChatPromptTemplate.from_messages(
149
- [("system", "You are helpful assistant"), ("user", "{input}")]
150
- )
151
- model = ChatOpenAI(model="gpt-3.5-turbo")
152
-
153
- chain = prompt | model
154
- chain.invoke(
155
- {"input": "tell me a short joke"},
156
- {"metadata": {"user_id": "1234", "session_id": 456}},
157
- )
158
-
159
- test_workflow_external()
160
-
161
- spans = exporter.get_finished_spans()
162
-
163
- assert [
164
- "ChatPromptTemplate.task",
165
- "ChatOpenAI.chat",
166
- "RunnableSequence.workflow",
167
- "test_workflow_external.workflow",
168
- ] == [span.name for span in spans], [span.name for span in spans]
169
-
170
- workflow_span = next(
171
- span for span in spans if span.name == "RunnableSequence.workflow"
172
- )
173
- prompt_span = next(span for span in spans if span.name == "ChatPromptTemplate.task")
174
- chat_span = next(span for span in spans if span.name == "ChatOpenAI.chat")
175
-
176
- assert (
177
- workflow_span.attributes[
178
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id"
179
- ]
180
- == "1234"
181
- )
182
- assert (
183
- workflow_span.attributes[
184
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id"
185
- ]
186
- == 456
187
- )
188
- assert (
189
- workflow_span.attributes[
190
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.workspace_id"
191
- ]
192
- == "789"
193
- )
194
- assert (
195
- chat_span.attributes[
196
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id"
197
- ]
198
- == "1234"
199
- )
200
- assert (
201
- chat_span.attributes[
202
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id"
203
- ]
204
- == 456
205
- )
206
- assert (
207
- chat_span.attributes[
208
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.workspace_id"
209
- ]
210
- == "789"
211
- )
212
- assert (
213
- prompt_span.attributes[
214
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id"
215
- ]
216
- == "1234"
217
- )
218
- assert (
219
- prompt_span.attributes[
220
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id"
221
- ]
222
- == 456
223
- )
224
- assert (
225
- prompt_span.attributes[
226
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.workspace_id"
227
- ]
228
- == "789"
229
- )
1
+ # import pytest
2
+ # from langchain_openai import ChatOpenAI
3
+ # from langchain.prompts import ChatPromptTemplate
4
+ # from opentelemetry.semconv_ai import SpanAttributes
5
+ # from lmnr.traceloop_sdk import Traceloop
6
+ # from lmnr.traceloop_sdk.decorators import task, workflow
7
+
8
+
9
+ # def test_association_properties(exporter):
10
+ # @workflow(name="test_workflow")
11
+ # def test_workflow():
12
+ # return test_task()
13
+
14
+ # @task(name="test_task")
15
+ # def test_task():
16
+ # return
17
+
18
+ # Traceloop.set_association_properties({"user_id": 1, "user_name": "John Doe"})
19
+ # test_workflow()
20
+
21
+ # spans = exporter.get_finished_spans()
22
+ # assert [span.name for span in spans] == [
23
+ # "test_task.task",
24
+ # "test_workflow.workflow",
25
+ # ]
26
+
27
+ # some_task_span = spans[0]
28
+ # some_workflow_span = spans[1]
29
+ # assert (
30
+ # some_workflow_span.attributes[
31
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id"
32
+ # ]
33
+ # == 1
34
+ # )
35
+ # assert (
36
+ # some_workflow_span.attributes[
37
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_name"
38
+ # ]
39
+ # == "John Doe"
40
+ # )
41
+ # assert (
42
+ # some_task_span.attributes[
43
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id"
44
+ # ]
45
+ # == 1
46
+ # )
47
+ # assert (
48
+ # some_task_span.attributes[
49
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_name"
50
+ # ]
51
+ # == "John Doe"
52
+ # )
53
+
54
+
55
+ # def test_association_properties_within_workflow(exporter):
56
+ # @workflow(name="test_workflow_within")
57
+ # def test_workflow():
58
+ # Traceloop.set_association_properties({"session_id": 15})
59
+ # return
60
+
61
+ # test_workflow()
62
+
63
+ # spans = exporter.get_finished_spans()
64
+ # assert [span.name for span in spans] == [
65
+ # "test_workflow_within.workflow",
66
+ # ]
67
+
68
+ # some_workflow_span = spans[0]
69
+ # assert (
70
+ # some_workflow_span.attributes[
71
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id"
72
+ # ]
73
+ # == 15
74
+ # )
75
+
76
+
77
+ # @pytest.mark.vcr
78
+ # def test_langchain_association_properties(exporter):
79
+ # prompt = ChatPromptTemplate.from_messages(
80
+ # [("system", "You are helpful assistant"), ("user", "{input}")]
81
+ # )
82
+ # model = ChatOpenAI(model="gpt-3.5-turbo")
83
+
84
+ # chain = prompt | model
85
+ # chain.invoke(
86
+ # {"input": "tell me a short joke"},
87
+ # {"metadata": {"user_id": "1234", "session_id": 456}},
88
+ # )
89
+
90
+ # spans = exporter.get_finished_spans()
91
+
92
+ # assert [
93
+ # "ChatPromptTemplate.task",
94
+ # "ChatOpenAI.chat",
95
+ # "RunnableSequence.workflow",
96
+ # ] == [span.name for span in spans], [span.name for span in spans]
97
+
98
+ # workflow_span = next(
99
+ # span for span in spans if span.name == "RunnableSequence.workflow"
100
+ # )
101
+ # prompt_span = next(span for span in spans if span.name == "ChatPromptTemplate.task")
102
+ # chat_span = next(span for span in spans if span.name == "ChatOpenAI.chat")
103
+
104
+ # assert (
105
+ # workflow_span.attributes[
106
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id"
107
+ # ]
108
+ # == "1234"
109
+ # )
110
+ # assert (
111
+ # workflow_span.attributes[
112
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id"
113
+ # ]
114
+ # == 456
115
+ # )
116
+ # assert (
117
+ # chat_span.attributes[
118
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id"
119
+ # ]
120
+ # == "1234"
121
+ # )
122
+ # assert (
123
+ # chat_span.attributes[
124
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id"
125
+ # ]
126
+ # == 456
127
+ # )
128
+ # assert (
129
+ # prompt_span.attributes[
130
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id"
131
+ # ]
132
+ # == "1234"
133
+ # )
134
+ # assert (
135
+ # prompt_span.attributes[
136
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id"
137
+ # ]
138
+ # == 456
139
+ # )
140
+
141
+
142
+ # @pytest.mark.vcr
143
+ # def test_langchain_and_external_association_properties(exporter):
144
+ # @workflow(name="test_workflow_external")
145
+ # def test_workflow_external():
146
+ # Traceloop.set_association_properties({"workspace_id": "789"})
147
+
148
+ # prompt = ChatPromptTemplate.from_messages(
149
+ # [("system", "You are helpful assistant"), ("user", "{input}")]
150
+ # )
151
+ # model = ChatOpenAI(model="gpt-3.5-turbo")
152
+
153
+ # chain = prompt | model
154
+ # chain.invoke(
155
+ # {"input": "tell me a short joke"},
156
+ # {"metadata": {"user_id": "1234", "session_id": 456}},
157
+ # )
158
+
159
+ # test_workflow_external()
160
+
161
+ # spans = exporter.get_finished_spans()
162
+
163
+ # assert [
164
+ # "ChatPromptTemplate.task",
165
+ # "ChatOpenAI.chat",
166
+ # "RunnableSequence.workflow",
167
+ # "test_workflow_external.workflow",
168
+ # ] == [span.name for span in spans], [span.name for span in spans]
169
+
170
+ # workflow_span = next(
171
+ # span for span in spans if span.name == "RunnableSequence.workflow"
172
+ # )
173
+ # prompt_span = next(span for span in spans if span.name == "ChatPromptTemplate.task")
174
+ # chat_span = next(span for span in spans if span.name == "ChatOpenAI.chat")
175
+
176
+ # assert (
177
+ # workflow_span.attributes[
178
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id"
179
+ # ]
180
+ # == "1234"
181
+ # )
182
+ # assert (
183
+ # workflow_span.attributes[
184
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id"
185
+ # ]
186
+ # == 456
187
+ # )
188
+ # assert (
189
+ # workflow_span.attributes[
190
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.workspace_id"
191
+ # ]
192
+ # == "789"
193
+ # )
194
+ # assert (
195
+ # chat_span.attributes[
196
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id"
197
+ # ]
198
+ # == "1234"
199
+ # )
200
+ # assert (
201
+ # chat_span.attributes[
202
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id"
203
+ # ]
204
+ # == 456
205
+ # )
206
+ # assert (
207
+ # chat_span.attributes[
208
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.workspace_id"
209
+ # ]
210
+ # == "789"
211
+ # )
212
+ # assert (
213
+ # prompt_span.attributes[
214
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id"
215
+ # ]
216
+ # == "1234"
217
+ # )
218
+ # assert (
219
+ # prompt_span.attributes[
220
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id"
221
+ # ]
222
+ # == 456
223
+ # )
224
+ # assert (
225
+ # prompt_span.attributes[
226
+ # f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.workspace_id"
227
+ # ]
228
+ # == "789"
229
+ # )