levelapp 0.1.4__py3-none-any.whl → 0.1.5__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.

Potentially problematic release.


This version of levelapp might be problematic. Click here for more details.

@@ -111,7 +111,7 @@ class DynamicModelBuilder:
111
111
  """
112
112
  if isinstance(value, Mapping):
113
113
  nested_model = self.create_dynamic_model(model_name=f"{model_name}_{key}", data=value)
114
- return nested_model, ...
114
+ return Optional[nested_model], None
115
115
 
116
116
  elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
117
117
  if not value:
@@ -119,15 +119,15 @@ class DynamicModelBuilder:
119
119
 
120
120
  elif isinstance(value[0], Mapping):
121
121
  nested_model = self.create_dynamic_model(model_name=f"{model_name}_{key}", data=value[0])
122
- return List[nested_model], ...
122
+ return Optional[List[nested_model]], None
123
123
 
124
124
  else:
125
125
  field_type = type(value[0]) if value[0] is not None else Any
126
- return List[field_type], ...
126
+ return Optional[List[field_type]], None
127
127
 
128
128
  else:
129
129
  field_type = Optional[type(value)] if value is not None else Optional[Any]
130
- return field_type, ...
130
+ return field_type, None
131
131
 
132
132
  def create_dynamic_model(self, model_name: str, data: Any) -> Type[BaseModel]:
133
133
  """
@@ -0,0 +1,156 @@
1
+ # api_config.yaml
2
+ # REST API Configuration Manager - Example Configuration
3
+
4
+ endpoints:
5
+ # Example 1: Simple GET endpoint with authentication
6
+ - name: github-user
7
+ base_url: https://api.github.com
8
+ path: /users/octocat
9
+ method: GET
10
+ timeout: 15
11
+ retry_count: 2
12
+ retry_backoff: 0.5
13
+ headers:
14
+ - name: Accept
15
+ value: application/vnd.github.v3+json
16
+ secure: false
17
+ - name: Authorization
18
+ value: GITHUB_TOKEN # Environment variable name
19
+ secure: true
20
+ request_schema: []
21
+ response_mapping:
22
+ - field_path: login
23
+ extract_as: username
24
+ - field_path: id
25
+ extract_as: user_id
26
+ - field_path: public_repos
27
+ extract_as: repo_count
28
+ default: 0
29
+
30
+ # Example 2: POST endpoint with nested payload
31
+ - name: user-creation-api
32
+ base_url: https://api.example.com
33
+ path: /v1/users
34
+ method: POST
35
+ timeout: 30
36
+ retry_count: 3
37
+ retry_backoff: 1.0
38
+ headers:
39
+ - name: Content-Type
40
+ value: application/json
41
+ secure: false
42
+ - name: X-API-Key
43
+ value: API_SECRET_KEY
44
+ secure: true
45
+ - name: X-Request-ID
46
+ value: unique-request-id-001
47
+ secure: false
48
+ request_schema:
49
+ - field_path: user.email
50
+ value: test@example.com
51
+ value_type: static
52
+ required: true
53
+ - field_path: user.name
54
+ value: user_name # Will be taken from context
55
+ value_type: dynamic
56
+ required: true
57
+ - field_path: user.metadata.source
58
+ value: api-config-manager
59
+ value_type: static
60
+ required: false
61
+ - field_path: user.metadata.environment
62
+ value: ENVIRONMENT
63
+ value_type: env
64
+ required: false
65
+ response_mapping:
66
+ - field_path: data.user.id
67
+ extract_as: created_user_id
68
+ - field_path: data.user.status
69
+ extract_as: user_status
70
+ default: unknown
71
+ - field_path: data.created_at
72
+ extract_as: timestamp
73
+
74
+ # Example 3: Complex nested response extraction
75
+ - name: search-api
76
+ base_url: https://api.service.com
77
+ path: /search
78
+ method: GET
79
+ timeout: 20
80
+ retry_count: 2
81
+ retry_backoff: 1.5
82
+ headers:
83
+ - name: Authorization
84
+ value: Bearer your-token-here
85
+ secure: false
86
+ request_schema: []
87
+ response_mapping:
88
+ - field_path: results[0].id
89
+ extract_as: first_result_id
90
+ default: null
91
+ - field_path: results[0].title
92
+ extract_as: first_result_title
93
+ default: "No results"
94
+ - field_path: meta.total
95
+ extract_as: total_results
96
+ default: 0
97
+ - field_path: meta.page
98
+ extract_as: current_page
99
+ default: 1
100
+
101
+ # Example 4: Health check endpoint (minimal config)
102
+ - name: health-check
103
+ base_url: https://status.example.com
104
+ path: /health
105
+ method: GET
106
+ timeout: 5
107
+ retry_count: 1
108
+ retry_backoff: 0.5
109
+ headers: []
110
+ request_schema: []
111
+ response_mapping:
112
+ - field_path: status
113
+ extract_as: service_status
114
+ default: down
115
+ - field_path: version
116
+ extract_as: service_version
117
+
118
+ # Example 5: PUT endpoint with authentication
119
+ - name: update-resource
120
+ base_url: https://api.platform.io
121
+ path: /v2/resources/12345
122
+ method: PUT
123
+ timeout: 25
124
+ retry_count: 3
125
+ retry_backoff: 2.0
126
+ headers:
127
+ - name: Content-Type
128
+ value: application/json
129
+ secure: false
130
+ - name: Authorization
131
+ value: PLATFORM_API_TOKEN
132
+ secure: true
133
+ - name: X-Client-Version
134
+ value: "1.0.0"
135
+ secure: false
136
+ request_schema:
137
+ - field_path: resource.name
138
+ value: resource_name
139
+ value_type: dynamic
140
+ required: true
141
+ - field_path: resource.attributes.priority
142
+ value: high
143
+ value_type: static
144
+ required: false
145
+ - field_path: resource.attributes.updated_by
146
+ value: USER_EMAIL
147
+ value_type: env
148
+ required: false
149
+ response_mapping:
150
+ - field_path: data.id
151
+ extract_as: resource_id
152
+ - field_path: data.updated_at
153
+ extract_as: last_updated
154
+ - field_path: data.attributes.status
155
+ extract_as: resource_status
156
+ default: pending
@@ -0,0 +1,94 @@
1
+ # api_config.yaml
2
+ # REST API Configuration Manager - Example Configuration
3
+
4
+ endpoints:
5
+ - name: dashq
6
+ base_url: https://dashq-gateway-485vb8zi.uc.gateway.dev
7
+ path: /api/conversations/events
8
+ method: POST
9
+ timeout: 60
10
+ retry_count: 3
11
+ retry_backoff: 0.5
12
+ headers:
13
+ - name: model_id
14
+ value: meta-llama/Meta-Llama-3.1-8B-Instruct
15
+ secure: false
16
+ - name: x-api-key
17
+ value: AIzaSyAmL8blcS2hpPrEH2b84B8ugsVoV7AXrfc
18
+ secure: false # change to true later and place the value in .env file
19
+ request_schema:
20
+ - field_path: eventType
21
+ value: newConversation
22
+ value_type: static
23
+ required: true
24
+ - field_path: conversationId
25
+ value: conversation_id
26
+ value_type: dynamic
27
+ required: true
28
+ - field_path: payload.messageType
29
+ value: newInquiry
30
+ value_type: static
31
+ required: true
32
+ - field_path: payload.communityId
33
+ value: 3310
34
+ value_type: static
35
+ required: true
36
+ - field_path: payload.accountId
37
+ value: 1440
38
+ value_type: static
39
+ required: true
40
+ - field_path: payload.prospectFirstName
41
+ value: Kanye
42
+ value_type: static
43
+ required: true
44
+ - field_path: payload.prospectLastName
45
+ value: West
46
+ value_type: static
47
+ required: true
48
+ - field_path: payload.message
49
+ value: user_message
50
+ value_type: dynamic
51
+ required: true
52
+ - field_path: payload.datetime
53
+ value: "2025-06-25T11:12:27.245Z"
54
+ value_type: static
55
+ required: true
56
+ - field_path: payload.inboundChannel
57
+ value: text
58
+ value_type: static
59
+ required: true
60
+ - field_path: payload.outboundChannel
61
+ value: text
62
+ value_type: static
63
+ required: true
64
+ - field_path: payload.inquirySource
65
+ value: test.com
66
+ value_type: static
67
+ required: true
68
+ response_mapping:
69
+ - field_path: eventType
70
+ extract_as: guardrail_flag
71
+ # - field_path: conversationId
72
+ # extract_as: conversation_id
73
+ # - field_path: payload.accountId
74
+ # extract_as: account_id
75
+ # - field_path: payload.messageType
76
+ # extract_as: message_type
77
+ - field_path: message
78
+ extract_as: agent_reply
79
+ # - field_path: payload.datetime
80
+ # extract_as: datetime
81
+ - field_path: metadata
82
+ extract_as: metadata
83
+ # - field_path: payload.metadata.inquirySource
84
+ # extract_as: inquiry_source
85
+ # - field_path: payload.metadata.booking
86
+ # extract_as: booking
87
+ # - field_path: payload.metadata.tasks
88
+ # extract_as: tasks
89
+ # - field_path: payload.metadata.communityId
90
+ # extract_as: community_id
91
+ # - field_path: payload.handoffMetadata
92
+ # extract_as: handoff_metadata
93
+ # - field_path: payload.isConversationOver
94
+ # extract_as: is_conversation_over
@@ -1,15 +1,21 @@
1
- from abc import ABC
1
+ import json
2
+ import logging
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
2
5
  from enum import Enum
3
- from typing import Any, List
6
+ from pathlib import Path
7
+ from typing import Any, List, Dict
4
8
 
5
- from pydantic import BaseModel, Field
9
+ import httpx
10
+ import yaml
11
+ from pydantic import BaseModel, Field, ValidationError
6
12
 
7
13
 
8
14
  class HttpMethod(str, Enum):
9
15
  GET = "GET"
10
16
  POST = "POST"
11
17
  PUT = "PUT"
12
- Patch = "PATCH"
18
+ PATCH = "PATCH"
13
19
  DELETE = "DELETE"
14
20
 
15
21
 
@@ -59,4 +65,318 @@ class EndpointConfig(BaseModel):
59
65
 
60
66
 
61
67
  class PayloadBuilder(ABC):
62
- """Abstract base for payload construction strategies."""
68
+ """Abstract base for payload construction strategies."""
69
+ @abstractmethod
70
+ def build(self, schema: List[RequestSchemaConfig], context: Dict[str, Any]) -> Dict[str, Any]:
71
+ """Build request payload from schema and context."""
72
+ pass
73
+
74
+
75
+ class JsonPathPayloadBuilder(PayloadBuilder):
76
+ """Builds nested JSON payloads using dot-notation paths."""
77
+ def build(self, schema: List[RequestSchemaConfig], context: Dict[str, Any]) -> Dict[str, Any]:
78
+ payload = {}
79
+
80
+ for field_config in schema:
81
+ value = self._resolve_value(field_config, context)
82
+ if value is None and field_config.required:
83
+ raise ValueError(f"Required field {field_config.field_path} has no value")
84
+
85
+ self._set_nested_value(payload, field_config.field_path, value)
86
+
87
+ return payload
88
+
89
+ @staticmethod
90
+ def _resolve_value(config: RequestSchemaConfig, context: Dict[str, Any]) -> Any:
91
+ """Resolve value based on type: statis, env, or dynamic."""
92
+ if config.value_type == "static":
93
+ return config.value
94
+ elif config.value_type == "env":
95
+ import os
96
+ return os.getenv(config.value)
97
+ elif config.value_type == "dynamic":
98
+ print(f"config value: {config.field_path}")
99
+ print(f"context: {context}")
100
+ return context.get(config.field_path)
101
+
102
+ return config.value
103
+
104
+ @staticmethod
105
+ def _set_nested_value(obj: Dict, path: str, value: Any) -> None:
106
+ """Set value in nested dict using dot notation."""
107
+ parts = path.split('.')
108
+ for part in parts[:-1]:
109
+ obj = obj.setdefault(part, {})
110
+
111
+ obj[parts[-1]] = value
112
+
113
+
114
+ class ResponseExtractor:
115
+ """Extracts data from API responses based on mapping config."""
116
+ def extract(
117
+ self,
118
+ response_data: Dict[str, Any],
119
+ mappings: List[ResponseMappingConfig]
120
+ ) -> Dict[str, Any]:
121
+ """Extract mapped fields from response data."""
122
+ result = {}
123
+
124
+ for mapping in mappings:
125
+ try:
126
+ value = self._extract_by_path(response_data, mapping.field_path)
127
+ result[mapping.extract_as] = value if value is not None else mapping.default
128
+
129
+ except Exception as e:
130
+ print(f"Failed to extract '{mapping.field_path}':\n{e}")
131
+ result[mapping.extract_as] = mapping.default
132
+
133
+ return result
134
+
135
+ @staticmethod
136
+ def _extract_by_path(obj: Any, path: str) -> Any:
137
+ """Extract value using JSONPath-like notation."""
138
+ parts = path.split('.')
139
+ current = obj
140
+
141
+ for part in parts:
142
+ if '[' in part and ']' in part:
143
+ key, idx = part.split('[')
144
+ idx = int(idx.rstrip(']'))
145
+ current = current[key][idx] if key else current[idx]
146
+ else:
147
+ current = current[part]
148
+
149
+ return current
150
+
151
+
152
+ @dataclass
153
+ class APIClient:
154
+ """HTTP client for REST API interactions."""
155
+ config: EndpointConfig
156
+ client: httpx.Client = field(init=False)
157
+ logger: logging.Logger = field(init=False)
158
+
159
+ def __post_init__(self):
160
+ self.logger = logging.getLogger(f"APIClient.{self.config.name}")
161
+ self.client = httpx.Client(
162
+ base_url=self.config.base_url,
163
+ timeout=self.config.timeout,
164
+ follow_redirects=True
165
+ )
166
+
167
+ def __del__(self):
168
+ if hasattr(self, "client"):
169
+ self.client.close()
170
+
171
+ def _build_headers(self) -> Dict[str, str]:
172
+ """Build headers with secure value resolution."""
173
+ headers = {}
174
+ import os
175
+
176
+ for header in self.config.headers:
177
+ if header.secure:
178
+ value = os.getenv(header.value)
179
+ if value is None:
180
+ self.logger.warning(f"Secure header '{header.name}' env var '{header.value}' not found")
181
+ continue
182
+ headers[header.name] = value
183
+ else:
184
+ headers[header.name] = header.value
185
+
186
+ return headers
187
+
188
+ def execute(
189
+ self,
190
+ payload: Dict[str, Any] | None = None,
191
+ query_params: Dict[str, Any] | None = None
192
+ ) -> httpx.Response:
193
+ """Execute API request with retry logic."""
194
+ headers = self._build_headers()
195
+
196
+ for attempt in range(self.config.retry_count):
197
+ try:
198
+ response = self.client.request(
199
+ method=self.config.method.value,
200
+ url=self.config.path,
201
+ json=payload,
202
+ params=query_params,
203
+ headers=headers
204
+ )
205
+ response.raise_for_status()
206
+ return response
207
+
208
+ except httpx.HTTPStatusError as e:
209
+ self.logger.error(f"HTTP {e.response.status_code}: {e.response.text}")
210
+ if attempt == self.config.retry_count:
211
+ raise e
212
+
213
+ except httpx.RequestError as e:
214
+ self.logger.error(f"Request failed (attempt {attempt + 1}): {e}")
215
+ if attempt == self.config.retry_count - 1:
216
+ raise
217
+
218
+ import time
219
+ time.sleep(self.config.retry_backoff * (attempt + 1))
220
+
221
+ raise RuntimeError("Max retries exceeded")
222
+
223
+
224
+ class ConnectivityTester:
225
+ """Tests REST endpoint connectivity with configurable behavior."""
226
+ def __init__(self, config: EndpointConfig):
227
+ self.config = config
228
+ self.client = APIClient(config=config)
229
+ self.payload_builder = JsonPathPayloadBuilder()
230
+ self.response_extractor = ResponseExtractor()
231
+ self.logger = logging.getLogger(f"ConnectivityTester.{self.config.name}")
232
+
233
+ def test(self, context: Dict[str, Any] = None) -> Dict[str, Any]:
234
+ """Execute connectivity test (template Method)."""
235
+ context = context or {}
236
+
237
+ self.logger.info(f"Starting connectivity test for '{self.config.name}'")
238
+
239
+ try:
240
+ payload = None
241
+ if self.config.request_schema:
242
+ payload = self.payload_builder.build(schema=self.config.request_schema, context=context)
243
+ # self.logger.debug(f"Request payload:\n{json.dumps(payload, indent=2, default=str)}\n---")
244
+ self.logger.debug(f"Request payload:\n{payload}\n---")
245
+
246
+ response = self.client.execute(payload=payload)
247
+ self.logger.info(f"Response status: {response.status_code}")
248
+
249
+ response_data = response.json() if response.text else {}
250
+ extracted = self.response_extractor.extract(
251
+ response_data=response_data,
252
+ mappings=self.config.response_mapping
253
+ )
254
+
255
+ return {
256
+ "success": True,
257
+ "status_code": response.status_code,
258
+ "extracted_data": extracted,
259
+ "raw_response": response_data
260
+ }
261
+
262
+ except Exception as e:
263
+ self.logger.error(f"Connectivity test failed: {e}", exc_info=e)
264
+ return {
265
+ "success": False,
266
+ "error": str(e),
267
+ "error_type": type(e).__name__
268
+ }
269
+
270
+
271
+ class ConfigurationManager:
272
+ """Manages endpoint configurations and creates testers."""
273
+ def __init__(self, config_path: Path):
274
+ self.config_path = config_path
275
+ self.endpoints: Dict[str, EndpointConfig] = {}
276
+ self.logger = logging.getLogger("ConfigurationManager")
277
+ self._load_config()
278
+
279
+ def _load_config(self) -> None:
280
+ """Load and validate YAML configuration."""
281
+ try:
282
+ with open(self.config_path, "r") as f:
283
+ data = yaml.safe_load(f)
284
+
285
+ for endpoint_data in data.get('endpoints', []):
286
+ config = EndpointConfig.model_validate(endpoint_data)
287
+ self.endpoints[config.name] = config
288
+ self.logger.info(f"Loaded endpoint config: {config.name}")
289
+
290
+ except Exception as e:
291
+ self.logger.error(f"Failed to load endpoint config: {e}", exc_info=e)
292
+ raise
293
+
294
+ def build_response_mapping(self, content: List[Dict[str, Any]]) -> List[ResponseMappingConfig]:
295
+ mappings = []
296
+ for el in content:
297
+ try:
298
+ mappings.append(ResponseMappingConfig.model_validate(el))
299
+ except ValidationError as e:
300
+ self.logger.error(f"Failed to validate response mapping: {e}", exc_info=e)
301
+
302
+ return mappings
303
+
304
+ def extract_response_data(
305
+ self,
306
+ endpoint_name: str,
307
+ context: Dict[str, Any],
308
+ mappings: List[ResponseMappingConfig]
309
+ ) -> Dict[str, Any]:
310
+ if endpoint_name not in self.endpoints:
311
+ raise ValueError(f"Endpoint '{endpoint_name}' not found in configuration")
312
+
313
+ payload_builder = JsonPathPayloadBuilder()
314
+ client = APIClient(config=self.endpoints[endpoint_name])
315
+ extractor = ResponseExtractor()
316
+ payload = payload_builder.build(
317
+ schema=self.endpoints[endpoint_name].request_schema,
318
+ context=context
319
+ )
320
+ response = client.execute(payload=payload)
321
+ self.logger.info(f"Response status: {response.status_code}")
322
+ response_data = response.json() if response.text else {}
323
+ extracted = extractor.extract(
324
+ response_data=response_data,
325
+ mappings=mappings
326
+ )
327
+ return extracted
328
+
329
+ def get_tester(self, endpoint_name: str) -> ConnectivityTester:
330
+ """Factory method: create connectivity tester for endpoint."""
331
+ if endpoint_name not in self.endpoints:
332
+ raise KeyError(f"Endpoint '{endpoint_name}' not found in configuration")
333
+
334
+ return ConnectivityTester(self.endpoints[endpoint_name])
335
+
336
+ def test_all(self, context: Dict[str, Any] | None = None) -> Dict[str, Dict[str, Any]]:
337
+ """Test all configured endpoints."""
338
+ results = {}
339
+ for name in self.endpoints:
340
+ tester = self.get_tester(name)
341
+ results[name] = tester.test(context)
342
+
343
+ return results
344
+
345
+
346
+ if __name__ == '__main__':
347
+ # Example: Load configuration and test endpoint
348
+ config_manager = ConfigurationManager(Path("dashq_api.yaml"))
349
+
350
+ # # Test specific endpoint
351
+ # tester = config_manager.get_tester("dashq")
352
+ # result = tester.test(context={"conversationId": "439484ef-403b-43c5-9908-884486149d0b"})
353
+ #
354
+ # print(json.dumps(result, indent=2))
355
+
356
+ # # Test all endpoints
357
+ # all_results = config_manager.test_all(context={"environment": "production"})
358
+ # print(json.dumps(all_results, indent=2))
359
+
360
+ content_ = [
361
+ {
362
+ "field_path": "payload.message",
363
+ "extract_as": "agent_reply"
364
+ },
365
+ {
366
+ "field_path": "payload.metadata",
367
+ "extract_as": "generated_metadata"
368
+ },
369
+ {
370
+ "field_path": "payload.handoffMetadata",
371
+ "extract_as": "handoff_metadata"
372
+ }
373
+ ]
374
+
375
+ mappings_ = config_manager.build_response_mapping(content=content_)
376
+ response_data_ = config_manager.extract_response_data(
377
+ endpoint_name="dashq",
378
+ context={"conversationId": "439484ef-403b-43c5-9908-884486149d0b"},
379
+ mappings=mappings_
380
+ )
381
+
382
+ print(f"extracted response data:\n{response_data_}")
@@ -0,0 +1,47 @@
1
+ endpoints:
2
+ - name: create-message
3
+ base_url: https://postman-echo.com
4
+ path: /post
5
+ method: POST
6
+ timeout: 15
7
+ retry_count: 2
8
+ retry_backoff: 0.5
9
+
10
+ headers:
11
+ - name: Content-Type
12
+ value: application/json
13
+ secure: false
14
+
15
+ request_schema:
16
+ # Static field
17
+ - field_path: message.source
18
+ value: system
19
+ value_type: static
20
+ required: true
21
+
22
+ # Dynamic field (from runtime context)
23
+ - field_path: message.user
24
+ value: user_name
25
+ value_type: dynamic
26
+ required: true
27
+
28
+ - field_path: message.text
29
+ value: message_text
30
+ value_type: dynamic
31
+ required: true
32
+
33
+ # Env-based field (from OS environment)
34
+ - field_path: metadata.env
35
+ value: ENVIRONMENT
36
+ value_type: env
37
+ required: false
38
+
39
+ response_mapping:
40
+ - field_path: json.message
41
+ extract_as: sent_message
42
+ - field_path: headers.content-type
43
+ extract_as: content_type
44
+ - field_path: url
45
+ extract_as: request_url
46
+
47
+
levelapp/core/session.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """levelapp/core/session.py"""
2
+ import asyncio
2
3
  import threading
3
4
 
4
5
  from abc import ABC
@@ -226,6 +227,13 @@ class EvaluationSession:
226
227
  with self.step(step_name=f"{self.session_name}.collect_results", category=MetricType.RESULTS_COLLECTION):
227
228
  self.workflow.collect_results()
228
229
 
230
+ def run_connectivity_test(self, context: Dict[str, Any]) -> Dict[str, Any]:
231
+ if not self.workflow:
232
+ raise RuntimeError(f"{self._NAME} Workflow not initialized")
233
+
234
+ results = asyncio.run(self.workflow.test_connection(context=context))
235
+ return results
236
+
229
237
  def get_stats(self) -> Dict[str, Any]:
230
238
  if self.enable_monitoring:
231
239
  return {
File without changes