miso-client 0.2.0__py3-none-any.whl → 0.5.0__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 miso-client might be problematic. Click here for more details.
- miso_client/__init__.py +59 -3
- miso_client/errors.py +22 -1
- miso_client/models/__init__.py +4 -0
- miso_client/models/error_response.py +50 -0
- miso_client/models/filter.py +140 -0
- miso_client/models/pagination.py +66 -0
- miso_client/models/sort.py +25 -0
- miso_client/services/logger.py +7 -6
- miso_client/utils/data_masker.py +77 -5
- miso_client/utils/error_utils.py +104 -0
- miso_client/utils/filter.py +256 -0
- miso_client/utils/http_client.py +517 -212
- miso_client/utils/internal_http_client.py +471 -0
- miso_client/utils/pagination.py +157 -0
- miso_client/utils/sensitive_fields_loader.py +116 -0
- miso_client/utils/sort.py +116 -0
- {miso_client-0.2.0.dist-info → miso_client-0.5.0.dist-info}/METADATA +348 -3
- miso_client-0.5.0.dist-info/RECORD +33 -0
- miso_client-0.2.0.dist-info/RECORD +0 -23
- {miso_client-0.2.0.dist-info → miso_client-0.5.0.dist-info}/WHEEL +0 -0
- {miso_client-0.2.0.dist-info → miso_client-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {miso_client-0.2.0.dist-info → miso_client-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sensitive fields configuration loader for ISO 27001 compliance.
|
|
3
|
+
|
|
4
|
+
This module provides utilities to load and merge sensitive fields configuration
|
|
5
|
+
from JSON files, supporting custom configuration paths and environment variables.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
# Default path to sensitive fields config relative to this file
|
|
14
|
+
_DEFAULT_CONFIG_PATH = Path(__file__).parent / "sensitive_fields_config.json"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_sensitive_fields_config(
|
|
18
|
+
config_path: Optional[str] = None,
|
|
19
|
+
) -> Dict[str, Any]:
|
|
20
|
+
"""
|
|
21
|
+
Load sensitive fields configuration from JSON file.
|
|
22
|
+
|
|
23
|
+
Supports custom path via:
|
|
24
|
+
1. config_path parameter
|
|
25
|
+
2. MISO_SENSITIVE_FIELDS_CONFIG environment variable
|
|
26
|
+
3. Default path: miso_client/utils/sensitive_fields_config.json
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
config_path: Optional custom path to JSON config file
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Dictionary with 'fields' and 'fieldPatterns' keys
|
|
33
|
+
Returns empty dict if file cannot be loaded
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
>>> config = load_sensitive_fields_config()
|
|
37
|
+
>>> fields = config.get('fields', {})
|
|
38
|
+
"""
|
|
39
|
+
# Priority: parameter > environment variable > default
|
|
40
|
+
if config_path:
|
|
41
|
+
file_path = Path(config_path)
|
|
42
|
+
elif os.environ.get("MISO_SENSITIVE_FIELDS_CONFIG"):
|
|
43
|
+
file_path = Path(os.environ["MISO_SENSITIVE_FIELDS_CONFIG"])
|
|
44
|
+
else:
|
|
45
|
+
file_path = _DEFAULT_CONFIG_PATH
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
49
|
+
config = json.load(f)
|
|
50
|
+
# Validate structure
|
|
51
|
+
if isinstance(config, dict):
|
|
52
|
+
return config
|
|
53
|
+
return {}
|
|
54
|
+
except (FileNotFoundError, json.JSONDecodeError, IOError, OSError):
|
|
55
|
+
# File not found, invalid JSON, or permission error
|
|
56
|
+
# Return empty dict - fallback to hardcoded defaults
|
|
57
|
+
return {}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_sensitive_fields_array(
|
|
61
|
+
config_path: Optional[str] = None,
|
|
62
|
+
) -> List[str]:
|
|
63
|
+
"""
|
|
64
|
+
Get flattened array of all sensitive field names from configuration.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
config_path: Optional custom path to JSON config file
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Flattened list of all sensitive field names from all categories
|
|
71
|
+
Returns empty list if config cannot be loaded
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
>>> fields = get_sensitive_fields_array()
|
|
75
|
+
>>> assert 'password' in fields
|
|
76
|
+
>>> assert 'token' in fields
|
|
77
|
+
"""
|
|
78
|
+
config = load_sensitive_fields_config(config_path)
|
|
79
|
+
fields_dict = config.get("fields", {})
|
|
80
|
+
|
|
81
|
+
# Flatten all categories into single list
|
|
82
|
+
all_fields: List[str] = []
|
|
83
|
+
if isinstance(fields_dict, dict):
|
|
84
|
+
for category_fields in fields_dict.values():
|
|
85
|
+
if isinstance(category_fields, list):
|
|
86
|
+
all_fields.extend(category_fields)
|
|
87
|
+
|
|
88
|
+
# Remove duplicates while preserving order
|
|
89
|
+
seen = set()
|
|
90
|
+
unique_fields = []
|
|
91
|
+
for field in all_fields:
|
|
92
|
+
if field.lower() not in seen:
|
|
93
|
+
seen.add(field.lower())
|
|
94
|
+
unique_fields.append(field)
|
|
95
|
+
|
|
96
|
+
return unique_fields
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_field_patterns(config_path: Optional[str] = None) -> List[str]:
|
|
100
|
+
"""
|
|
101
|
+
Get field pattern matching rules from configuration.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
config_path: Optional custom path to JSON config file
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
List of field pattern matching rules
|
|
108
|
+
Returns empty list if config cannot be loaded or no patterns defined
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
>>> patterns = get_field_patterns()
|
|
112
|
+
>>> # Patterns can be regex patterns or simple matching rules
|
|
113
|
+
"""
|
|
114
|
+
config = load_sensitive_fields_config(config_path)
|
|
115
|
+
patterns = config.get("fieldPatterns", [])
|
|
116
|
+
return patterns if isinstance(patterns, list) else []
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sort utilities for MisoClient SDK.
|
|
3
|
+
|
|
4
|
+
This module provides reusable sort utilities for parsing sort parameters
|
|
5
|
+
and building sort query strings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import List
|
|
9
|
+
from urllib.parse import quote
|
|
10
|
+
|
|
11
|
+
from ..models.sort import SortOption
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_sort_params(params: dict) -> List[SortOption]:
|
|
15
|
+
"""
|
|
16
|
+
Parse sort query parameters into SortOption list.
|
|
17
|
+
|
|
18
|
+
Parses `?sort=-field` format into SortOption objects.
|
|
19
|
+
Supports multiple sort parameters (array of sort strings).
|
|
20
|
+
Prefix with '-' for descending order, otherwise ascending.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
params: Dictionary with query parameters (e.g., {'sort': '-updated_at'} or {'sort': ['-updated_at', 'created_at']})
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
List of SortOption objects
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
>>> parse_sort_params({'sort': '-updated_at'})
|
|
30
|
+
[SortOption(field='updated_at', order='desc')]
|
|
31
|
+
>>> parse_sort_params({'sort': ['-updated_at', 'created_at']})
|
|
32
|
+
[SortOption(field='updated_at', order='desc'), SortOption(field='created_at', order='asc')]
|
|
33
|
+
"""
|
|
34
|
+
sort_options: List[SortOption] = []
|
|
35
|
+
|
|
36
|
+
# Get sort parameter (can be string or list)
|
|
37
|
+
sort_param = params.get("sort")
|
|
38
|
+
if not sort_param:
|
|
39
|
+
return sort_options
|
|
40
|
+
|
|
41
|
+
# Normalize to list
|
|
42
|
+
if isinstance(sort_param, str):
|
|
43
|
+
sort_strings = [sort_param]
|
|
44
|
+
elif isinstance(sort_param, list):
|
|
45
|
+
sort_strings = sort_param
|
|
46
|
+
else:
|
|
47
|
+
return sort_options
|
|
48
|
+
|
|
49
|
+
# Parse each sort string
|
|
50
|
+
for sort_str in sort_strings:
|
|
51
|
+
if not isinstance(sort_str, str):
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
sort_str = sort_str.strip()
|
|
55
|
+
if not sort_str:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
# Check for descending order (prefix with '-')
|
|
59
|
+
if sort_str.startswith("-"):
|
|
60
|
+
field = sort_str[1:].strip()
|
|
61
|
+
order = "desc"
|
|
62
|
+
else:
|
|
63
|
+
field = sort_str.strip()
|
|
64
|
+
order = "asc"
|
|
65
|
+
|
|
66
|
+
if field:
|
|
67
|
+
sort_options.append(SortOption(field=field, order=order))
|
|
68
|
+
|
|
69
|
+
return sort_options
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def build_sort_string(sort_options: List[SortOption]) -> str:
|
|
73
|
+
"""
|
|
74
|
+
Convert SortOption list to query string format.
|
|
75
|
+
|
|
76
|
+
Converts SortOption objects to sort query string format.
|
|
77
|
+
Descending order fields are prefixed with '-'.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
sort_options: List of SortOption objects
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Sort query string (e.g., '-updated_at,created_at' or single value '-updated_at')
|
|
84
|
+
|
|
85
|
+
Examples:
|
|
86
|
+
>>> from miso_client.models.sort import SortOption
|
|
87
|
+
>>> sort_options = [SortOption(field='updated_at', order='desc')]
|
|
88
|
+
>>> build_sort_string(sort_options)
|
|
89
|
+
'-updated_at'
|
|
90
|
+
>>> sort_options = [
|
|
91
|
+
... SortOption(field='updated_at', order='desc'),
|
|
92
|
+
... SortOption(field='created_at', order='asc')
|
|
93
|
+
... ]
|
|
94
|
+
>>> build_sort_string(sort_options)
|
|
95
|
+
'-updated_at,created_at'
|
|
96
|
+
"""
|
|
97
|
+
if not sort_options:
|
|
98
|
+
return ""
|
|
99
|
+
|
|
100
|
+
sort_strings: List[str] = []
|
|
101
|
+
for sort_option in sort_options:
|
|
102
|
+
field = sort_option.field
|
|
103
|
+
order = sort_option.order
|
|
104
|
+
|
|
105
|
+
# URL encode field name
|
|
106
|
+
field_encoded = quote(field)
|
|
107
|
+
|
|
108
|
+
# Add '-' prefix for descending order
|
|
109
|
+
if order == "desc":
|
|
110
|
+
sort_strings.append(f"-{field_encoded}")
|
|
111
|
+
else:
|
|
112
|
+
sort_strings.append(field_encoded)
|
|
113
|
+
|
|
114
|
+
# Join multiple sorts with comma (if needed for single sort param)
|
|
115
|
+
# Or return as comma-separated string
|
|
116
|
+
return ",".join(sort_strings)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: miso-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Python client SDK for AI Fabrix authentication, authorization, and logging
|
|
5
5
|
Home-page: https://github.com/aifabrix/miso-client-python
|
|
6
6
|
Author: AI Fabrix Team
|
|
@@ -80,10 +80,13 @@ The **AI Fabrix Miso Client SDK** provides authentication, authorization, and lo
|
|
|
80
80
|
### 📊 Compliance & Audit
|
|
81
81
|
|
|
82
82
|
**ISO 27001 Compliance**
|
|
83
|
-
- Comprehensive audit trails for all user actions
|
|
83
|
+
- Comprehensive audit trails for all user actions and HTTP requests
|
|
84
|
+
- Automatic data masking for all sensitive information in logs
|
|
85
|
+
- HTTP request/response audit logging with masked sensitive data
|
|
84
86
|
- Data access logging and monitoring
|
|
85
87
|
- Security event tracking
|
|
86
88
|
- Accountability and non-repudiation
|
|
89
|
+
- Configurable sensitive fields via JSON configuration
|
|
87
90
|
|
|
88
91
|
**Regulatory Compliance**
|
|
89
92
|
- GDPR-ready data protection
|
|
@@ -134,9 +137,13 @@ The **AI Fabrix Miso Client SDK** provides authentication, authorization, and lo
|
|
|
134
137
|
|
|
135
138
|
**Observability**
|
|
136
139
|
- Centralized logging with correlation IDs
|
|
140
|
+
- Automatic HTTP request/response audit logging (ISO 27001 compliant)
|
|
141
|
+
- Debug logging with detailed request/response information (when `log_level='debug'`)
|
|
137
142
|
- Performance tracking and metrics
|
|
138
143
|
- Error tracking and debugging
|
|
139
144
|
- Health monitoring
|
|
145
|
+
- Automatic data masking for sensitive information in logs
|
|
146
|
+
- Configurable sensitive fields via JSON configuration
|
|
140
147
|
|
|
141
148
|
---
|
|
142
149
|
|
|
@@ -262,7 +269,7 @@ if is_admin:
|
|
|
262
269
|
|
|
263
270
|
### Step 5: Activate Logging
|
|
264
271
|
|
|
265
|
-
**What happens:** Application logs are sent to the Miso Controller with client token authentication.
|
|
272
|
+
**What happens:** Application logs are sent to the Miso Controller with client token authentication. All HTTP requests are automatically audited with ISO 27001 compliant data masking.
|
|
266
273
|
|
|
267
274
|
```python
|
|
268
275
|
from miso_client import MisoClient, load_config
|
|
@@ -278,10 +285,17 @@ user = await client.get_user(token)
|
|
|
278
285
|
await client.log.info('User accessed dashboard', {'userId': user.id if user else None})
|
|
279
286
|
await client.log.error('Operation failed', {'error': str(err)})
|
|
280
287
|
await client.log.warn('Unusual activity', {'details': '...'})
|
|
288
|
+
|
|
289
|
+
# HTTP requests are automatically audited
|
|
290
|
+
# All sensitive data is automatically masked before logging
|
|
291
|
+
result = await client.http_client.get('/api/users')
|
|
292
|
+
# This automatically creates an audit log: http.request.GET with masked sensitive data
|
|
281
293
|
```
|
|
282
294
|
|
|
283
295
|
**What happens to logs?** They're sent to the Miso Controller for centralized monitoring and analysis. Client token is automatically included.
|
|
284
296
|
|
|
297
|
+
**ISO 27001 Compliance:** All HTTP requests are automatically audited with sensitive data masked. Set `log_level='debug'` to enable detailed request/response logging (all sensitive data is still masked).
|
|
298
|
+
|
|
285
299
|
→ [Complete logging example](examples/step-5-logging.py)
|
|
286
300
|
→ [Logging Reference](docs/api-reference.md#logger-service)
|
|
287
301
|
|
|
@@ -414,6 +428,7 @@ config = MisoClientConfig(
|
|
|
414
428
|
port=6379,
|
|
415
429
|
),
|
|
416
430
|
log_level="info", # Optional: 'debug' | 'info' | 'warn' | 'error'
|
|
431
|
+
# Set to 'debug' for detailed HTTP request/response logging
|
|
417
432
|
api_key="your-test-api-key", # Optional: API key for testing (bypasses OAuth2)
|
|
418
433
|
cache={ # Optional: Cache TTL settings
|
|
419
434
|
"role_ttl": 900, # Role cache TTL (default: 900s)
|
|
@@ -424,6 +439,18 @@ config = MisoClientConfig(
|
|
|
424
439
|
|
|
425
440
|
**Recommended:** Use `load_config()` to load from `.env` file automatically.
|
|
426
441
|
|
|
442
|
+
**ISO 27001 Data Masking Configuration:**
|
|
443
|
+
|
|
444
|
+
Sensitive fields are configured via `miso_client/utils/sensitive_fields_config.json`. You can customize this by:
|
|
445
|
+
|
|
446
|
+
1. Setting `MISO_SENSITIVE_FIELDS_CONFIG` environment variable to point to a custom JSON file
|
|
447
|
+
2. Using `DataMasker.set_config_path()` to set a custom path programmatically
|
|
448
|
+
|
|
449
|
+
The default configuration includes ISO 27001 compliant sensitive fields:
|
|
450
|
+
- Authentication: password, token, secret, key, auth, authorization
|
|
451
|
+
- PII: ssn, creditcard, cc, cvv, pin, otp
|
|
452
|
+
- Security: apikey, accesstoken, refreshtoken, privatekey, secretkey, cookie, session
|
|
453
|
+
|
|
427
454
|
→ [Complete Configuration Reference](docs/configuration.md)
|
|
428
455
|
|
|
429
456
|
---
|
|
@@ -448,6 +475,28 @@ The SDK consists of five core services:
|
|
|
448
475
|
- **LoggerService** - Centralized logging with API key authentication
|
|
449
476
|
- **RedisService** - Caching and queue management (optional)
|
|
450
477
|
|
|
478
|
+
### HTTP Client Architecture
|
|
479
|
+
|
|
480
|
+
The SDK uses a two-layer HTTP client architecture for ISO 27001 compliance:
|
|
481
|
+
|
|
482
|
+
- **InternalHttpClient** - Core HTTP functionality with automatic client token management (internal)
|
|
483
|
+
- **HttpClient** - Public wrapper that adds automatic ISO 27001 compliant audit and debug logging
|
|
484
|
+
|
|
485
|
+
**Features:**
|
|
486
|
+
- Automatic audit logging for all HTTP requests (`http.request.{METHOD}`)
|
|
487
|
+
- Debug logging when `log_level === 'debug'` with detailed request/response information
|
|
488
|
+
- Automatic data masking using `DataMasker` before logging (ISO 27001 compliant)
|
|
489
|
+
- Sensitive endpoints (`/api/logs`, `/api/auth/token`) are excluded from audit logging to prevent infinite loops
|
|
490
|
+
- All sensitive data (headers, bodies, query params) is automatically masked before logging
|
|
491
|
+
|
|
492
|
+
**ISO 27001 Compliance:**
|
|
493
|
+
- All request headers are masked (Authorization, x-client-token, Cookie, etc.)
|
|
494
|
+
- All request bodies are recursively masked for sensitive fields (password, token, secret, SSN, etc.)
|
|
495
|
+
- All response bodies are masked (limited to first 1000 characters)
|
|
496
|
+
- Query parameters are automatically masked
|
|
497
|
+
- Error messages are masked if they contain sensitive data
|
|
498
|
+
- Sensitive fields configuration can be customized via `sensitive_fields_config.json`
|
|
499
|
+
|
|
451
500
|
→ [Architecture Details](docs/api-reference.md#architecture)
|
|
452
501
|
|
|
453
502
|
---
|
|
@@ -485,6 +534,302 @@ The SDK consists of five core services:
|
|
|
485
534
|
- [Flask Decorators](docs/examples.md#flask-decorators) - Decorator-based auth
|
|
486
535
|
- [Error Handling](docs/examples.md#error-handling) - Best practices
|
|
487
536
|
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
### Structured Error Responses
|
|
540
|
+
|
|
541
|
+
**What happens:** The SDK automatically parses structured error responses from the API (RFC 7807-style format) and makes them available through the `MisoClientError` exception.
|
|
542
|
+
|
|
543
|
+
```python
|
|
544
|
+
from miso_client import MisoClient, MisoClientError, ErrorResponse, load_config
|
|
545
|
+
|
|
546
|
+
client = MisoClient(load_config())
|
|
547
|
+
await client.initialize()
|
|
548
|
+
|
|
549
|
+
try:
|
|
550
|
+
result = await client.http_client.get("/api/some-endpoint")
|
|
551
|
+
except MisoClientError as e:
|
|
552
|
+
# Check if structured error response is available
|
|
553
|
+
if e.error_response:
|
|
554
|
+
print(f"Error Type: {e.error_response.type}")
|
|
555
|
+
print(f"Error Title: {e.error_response.title}")
|
|
556
|
+
print(f"Status Code: {e.error_response.statusCode}")
|
|
557
|
+
print(f"Errors: {e.error_response.errors}")
|
|
558
|
+
print(f"Instance: {e.error_response.instance}")
|
|
559
|
+
else:
|
|
560
|
+
# Fallback to traditional error handling
|
|
561
|
+
print(f"Error: {e.message}")
|
|
562
|
+
print(f"Status Code: {e.status_code}")
|
|
563
|
+
print(f"Error Body: {e.error_body}")
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
**Error Response Structure:**
|
|
567
|
+
|
|
568
|
+
The `ErrorResponse` model follows RFC 7807-style format:
|
|
569
|
+
|
|
570
|
+
```json
|
|
571
|
+
{
|
|
572
|
+
"errors": [
|
|
573
|
+
"The user has provided input that the browser is unable to convert.",
|
|
574
|
+
"There are multiple rows in the database for the same value"
|
|
575
|
+
],
|
|
576
|
+
"type": "/Errors/Bad Input",
|
|
577
|
+
"title": "Bad Request",
|
|
578
|
+
"statusCode": 400,
|
|
579
|
+
"instance": "/OpenApi/rest/Xzy"
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
**Features:**
|
|
584
|
+
|
|
585
|
+
- **Automatic Parsing**: Structured error responses are automatically parsed from HTTP responses
|
|
586
|
+
- **Backward Compatible**: Falls back to traditional error handling when structured format is not available
|
|
587
|
+
- **Type Safety**: Full type hints with Pydantic models for reliable error handling
|
|
588
|
+
- **Generic Interface**: `ErrorResponse` model can be reused across different applications
|
|
589
|
+
- **Instance URI**: Automatically extracted from request URL if not provided in response
|
|
590
|
+
|
|
591
|
+
**Using ErrorResponse directly:**
|
|
592
|
+
|
|
593
|
+
```python
|
|
594
|
+
from miso_client import ErrorResponse
|
|
595
|
+
|
|
596
|
+
# Create ErrorResponse from dict
|
|
597
|
+
error_data = {
|
|
598
|
+
"errors": ["Validation failed"],
|
|
599
|
+
"type": "/Errors/Validation",
|
|
600
|
+
"title": "Validation Error",
|
|
601
|
+
"statusCode": 422,
|
|
602
|
+
"instance": "/api/endpoint"
|
|
603
|
+
}
|
|
604
|
+
error_response = ErrorResponse(**error_data)
|
|
605
|
+
|
|
606
|
+
# Access fields
|
|
607
|
+
print(error_response.errors) # ["Validation failed"]
|
|
608
|
+
print(error_response.type) # "/Errors/Validation"
|
|
609
|
+
print(error_response.title) # "Validation Error"
|
|
610
|
+
print(error_response.statusCode) # 422
|
|
611
|
+
print(error_response.instance) # "/api/endpoint"
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
### Pagination, Filtering, and Sorting Utilities
|
|
617
|
+
|
|
618
|
+
**What happens:** The SDK provides reusable utilities for pagination, filtering, sorting, and error handling that work with any API endpoint.
|
|
619
|
+
|
|
620
|
+
#### Pagination
|
|
621
|
+
|
|
622
|
+
**Pagination Parameters:**
|
|
623
|
+
- `page`: Page number (1-based, defaults to 1)
|
|
624
|
+
- `page_size`: Number of items per page (defaults to 25)
|
|
625
|
+
|
|
626
|
+
```python
|
|
627
|
+
from miso_client import (
|
|
628
|
+
parse_pagination_params,
|
|
629
|
+
create_paginated_list_response,
|
|
630
|
+
PaginatedListResponse,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# Parse pagination from query parameters
|
|
634
|
+
params = {"page": "1", "page_size": "25"}
|
|
635
|
+
current_page, page_size = parse_pagination_params(params)
|
|
636
|
+
|
|
637
|
+
# Create paginated response
|
|
638
|
+
items = [{"id": 1}, {"id": 2}]
|
|
639
|
+
response = create_paginated_list_response(
|
|
640
|
+
items,
|
|
641
|
+
total_items=120,
|
|
642
|
+
current_page=1,
|
|
643
|
+
page_size=25,
|
|
644
|
+
type="item"
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
# Response structure:
|
|
648
|
+
# {
|
|
649
|
+
# "meta": {
|
|
650
|
+
# "total_items": 120,
|
|
651
|
+
# "current_page": 1,
|
|
652
|
+
# "page_size": 25,
|
|
653
|
+
# "type": "item"
|
|
654
|
+
# },
|
|
655
|
+
# "data": [{"id": 1}, {"id": 2}]
|
|
656
|
+
# }
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
#### Filtering
|
|
660
|
+
|
|
661
|
+
**Filter Operators:** `eq`, `neq`, `in`, `nin`, `gt`, `lt`, `gte`, `lte`, `contains`, `like`
|
|
662
|
+
|
|
663
|
+
**Filter Format:** `field:op:value` (e.g., `status:eq:active`)
|
|
664
|
+
|
|
665
|
+
```python
|
|
666
|
+
from miso_client import FilterBuilder, parse_filter_params, build_query_string
|
|
667
|
+
|
|
668
|
+
# Dynamic filter building with FilterBuilder
|
|
669
|
+
filter_builder = FilterBuilder() \
|
|
670
|
+
.add('status', 'eq', 'active') \
|
|
671
|
+
.add('region', 'in', ['eu', 'us']) \
|
|
672
|
+
.add('created_at', 'gte', '2024-01-01')
|
|
673
|
+
|
|
674
|
+
# Get query string
|
|
675
|
+
query_string = filter_builder.to_query_string()
|
|
676
|
+
# Returns: "filter=status:eq:active&filter=region:in:eu,us&filter=created_at:gte:2024-01-01"
|
|
677
|
+
|
|
678
|
+
# Parse existing filter parameters
|
|
679
|
+
params = {'filter': ['status:eq:active', 'region:in:eu,us']}
|
|
680
|
+
filters = parse_filter_params(params)
|
|
681
|
+
# Returns: [FilterOption(field='status', op='eq', value='active'), ...]
|
|
682
|
+
|
|
683
|
+
# Use with HTTP client
|
|
684
|
+
response = await client.http_client.get_with_filters(
|
|
685
|
+
'/api/items',
|
|
686
|
+
filter_builder=filter_builder
|
|
687
|
+
)
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
**Building Complete Filter Queries:**
|
|
691
|
+
|
|
692
|
+
```python
|
|
693
|
+
from miso_client import FilterQuery, FilterOption, build_query_string
|
|
694
|
+
|
|
695
|
+
# Create filter query with filters, sort, pagination, and fields
|
|
696
|
+
filter_query = FilterQuery(
|
|
697
|
+
filters=[
|
|
698
|
+
FilterOption(field='status', op='eq', value='active'),
|
|
699
|
+
FilterOption(field='region', op='in', value=['eu', 'us'])
|
|
700
|
+
],
|
|
701
|
+
sort=['-updated_at', 'created_at'],
|
|
702
|
+
page=1,
|
|
703
|
+
page_size=25,
|
|
704
|
+
fields=['id', 'name', 'status']
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
# Build query string
|
|
708
|
+
query_string = build_query_string(filter_query)
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
#### Sorting
|
|
712
|
+
|
|
713
|
+
**Sort Format:** `-field` for descending, `field` for ascending (e.g., `-updated_at`, `created_at`)
|
|
714
|
+
|
|
715
|
+
```python
|
|
716
|
+
from miso_client import parse_sort_params, build_sort_string, SortOption
|
|
717
|
+
|
|
718
|
+
# Parse sort parameters
|
|
719
|
+
params = {'sort': '-updated_at'}
|
|
720
|
+
sort_options = parse_sort_params(params)
|
|
721
|
+
# Returns: [SortOption(field='updated_at', order='desc')]
|
|
722
|
+
|
|
723
|
+
# Parse multiple sorts
|
|
724
|
+
params = {'sort': ['-updated_at', 'created_at']}
|
|
725
|
+
sort_options = parse_sort_params(params)
|
|
726
|
+
# Returns: [
|
|
727
|
+
# SortOption(field='updated_at', order='desc'),
|
|
728
|
+
# SortOption(field='created_at', order='asc')
|
|
729
|
+
# ]
|
|
730
|
+
|
|
731
|
+
# Build sort string
|
|
732
|
+
sort_options = [
|
|
733
|
+
SortOption(field='updated_at', order='desc'),
|
|
734
|
+
SortOption(field='created_at', order='asc')
|
|
735
|
+
]
|
|
736
|
+
sort_string = build_sort_string(sort_options)
|
|
737
|
+
# Returns: "-updated_at,created_at"
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
#### Combined Usage
|
|
741
|
+
|
|
742
|
+
**Pagination + Filter + Sort:**
|
|
743
|
+
|
|
744
|
+
```python
|
|
745
|
+
from miso_client import (
|
|
746
|
+
FilterBuilder,
|
|
747
|
+
FilterQuery,
|
|
748
|
+
build_query_string,
|
|
749
|
+
parse_pagination_params,
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
# Build filters
|
|
753
|
+
filter_builder = FilterBuilder() \
|
|
754
|
+
.add('status', 'eq', 'active') \
|
|
755
|
+
.add('region', 'in', ['eu', 'us'])
|
|
756
|
+
|
|
757
|
+
# Parse pagination
|
|
758
|
+
params = {'page': '1', 'page_size': '25'}
|
|
759
|
+
current_page, page_size = parse_pagination_params(params)
|
|
760
|
+
|
|
761
|
+
# Create complete query
|
|
762
|
+
filter_query = FilterQuery(
|
|
763
|
+
filters=filter_builder.build(),
|
|
764
|
+
sort=['-updated_at'],
|
|
765
|
+
page=current_page,
|
|
766
|
+
page_size=page_size
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
# Build query string
|
|
770
|
+
query_string = build_query_string(filter_query)
|
|
771
|
+
|
|
772
|
+
# Use with HTTP client
|
|
773
|
+
response = await client.http_client.get_with_filters(
|
|
774
|
+
'/api/items',
|
|
775
|
+
filter_builder=filter_builder,
|
|
776
|
+
params={'page': current_page, 'page_size': page_size}
|
|
777
|
+
)
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
**Or use pagination helper:**
|
|
781
|
+
|
|
782
|
+
```python
|
|
783
|
+
# Get paginated response
|
|
784
|
+
response = await client.http_client.get_paginated(
|
|
785
|
+
'/api/items',
|
|
786
|
+
page=1,
|
|
787
|
+
page_size=25
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
# Response is automatically parsed as PaginatedListResponse
|
|
791
|
+
print(response.meta.total_items) # 120
|
|
792
|
+
print(response.meta.current_page) # 1
|
|
793
|
+
print(len(response.data)) # 25
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
#### Metadata Filter Integration
|
|
797
|
+
|
|
798
|
+
**Working with `/metadata/filter` endpoint:**
|
|
799
|
+
|
|
800
|
+
```python
|
|
801
|
+
# Get metadata filters from endpoint
|
|
802
|
+
metadata_response = await client.http_client.post(
|
|
803
|
+
"/api/v1/metadata/filter",
|
|
804
|
+
{"documentStorageKey": "my-doc-storage"}
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
# Convert AccessFieldFilter to FilterBuilder
|
|
808
|
+
filter_builder = FilterBuilder()
|
|
809
|
+
for access_filter in metadata_response.mandatoryFilters:
|
|
810
|
+
filter_builder.add(access_filter.field, 'in', access_filter.values)
|
|
811
|
+
|
|
812
|
+
# Use with query utilities
|
|
813
|
+
query_string = filter_builder.to_query_string()
|
|
814
|
+
|
|
815
|
+
# Apply to API requests
|
|
816
|
+
response = await client.http_client.get_with_filters(
|
|
817
|
+
'/api/items',
|
|
818
|
+
filter_builder=filter_builder
|
|
819
|
+
)
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
**Features:**
|
|
823
|
+
|
|
824
|
+
- **Snake_case Convention**: All utilities use snake_case to match Miso/Dataplane API
|
|
825
|
+
- **Type Safety**: Full type hints with Pydantic models
|
|
826
|
+
- **Dynamic Filtering**: FilterBuilder supports method chaining for complex filters
|
|
827
|
+
- **Local Testing**: `apply_filters()` and `apply_pagination_to_array()` for local filtering/pagination in tests
|
|
828
|
+
- **URL Encoding**: Automatic URL encoding for field names and values
|
|
829
|
+
- **Backward Compatible**: Works alongside existing HTTP client methods
|
|
830
|
+
|
|
831
|
+
---
|
|
832
|
+
|
|
488
833
|
### Common Tasks
|
|
489
834
|
|
|
490
835
|
**Add authentication middleware (FastAPI):**
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
miso_client/__init__.py,sha256=iDpRsYBbBDrLQFNJdtDlv4yrg1pvwNZpD2C3TYAyOSo,15595
|
|
2
|
+
miso_client/errors.py,sha256=uyS5j-_bUCA5gbINPYQd0wMpGsaEH0tJRK0obQTq2oo,1976
|
|
3
|
+
miso_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
miso_client/models/__init__.py,sha256=lMnzU7j2Z5_UehvOeIrbJIo3MH4j5KINGfU1vJTzTyU,139
|
|
5
|
+
miso_client/models/config.py,sha256=TLckmwajrdnJpMTsjCtaFkz8wFAXcEQJfKjXQ8ww6vM,8024
|
|
6
|
+
miso_client/models/error_response.py,sha256=YBQkchdWOo6NjdNpLlOvGaIhOwA1MJzkTS2oeU0e2Mk,1694
|
|
7
|
+
miso_client/models/filter.py,sha256=3XWVxAjtUP78KPocd4-Ko9SijeRXm3jWVVBCXDPnXuI,4111
|
|
8
|
+
miso_client/models/pagination.py,sha256=f-YbspLrQH3ZhDYzbMCTloeKF2EyaSi7zjFLgh6dhBQ,2078
|
|
9
|
+
miso_client/models/sort.py,sha256=CutVQnYC4uRgesJ8zgd4OmPGZIop1KzvtgiTjNproSM,621
|
|
10
|
+
miso_client/services/__init__.py,sha256=2ok62Z9kaS0Zze-OxRkxEJ4JidzN9jL_pzGMOxpZppQ,489
|
|
11
|
+
miso_client/services/auth.py,sha256=hYnHEoTNgeH_g0ItoVDq99fBZCZX0o-0o-9gRxqZYmw,5465
|
|
12
|
+
miso_client/services/cache.py,sha256=lXcLcRm56snOX3AQiEKi-j7FIikCLYnEK2HXxBkxm2M,6510
|
|
13
|
+
miso_client/services/encryption.py,sha256=8512ACLt0P8t3W_CGzmG0JRSvzDinvjPRvkifdDxIRs,3022
|
|
14
|
+
miso_client/services/logger.py,sha256=dopKQeBZpz9S3iX9vw8xcPsS1oyMASOUQ4-OvCdElTw,16772
|
|
15
|
+
miso_client/services/permission.py,sha256=GOGRAaXSfPKTXqEqbR0XP8hrA-YQgVFbHPxD-gGBUyA,6853
|
|
16
|
+
miso_client/services/redis.py,sha256=BWfgXoSOyyGrB9cf_kTY0lZwfbjUWQhkiOyFFSp590M,5348
|
|
17
|
+
miso_client/services/role.py,sha256=qmxhk54QUGCjCuCWm_ruDlTq7iT9yOicfUvdRVkFKUI,5517
|
|
18
|
+
miso_client/utils/__init__.py,sha256=HArSxVKrmCqFkqFOPwe1i3B2IBHJ1vRqYu98c_KASV0,366
|
|
19
|
+
miso_client/utils/config_loader.py,sha256=yZk4pXNIBu3i61KqxM8QwsjraM0xhqUcH2THl8-DMu0,3027
|
|
20
|
+
miso_client/utils/data_masker.py,sha256=D7AEyViGxoShLa5UUZHYhRCPQMPKqX7qNilTK9h87OM,7035
|
|
21
|
+
miso_client/utils/error_utils.py,sha256=B-BXmDTTj6II1HwjC_DE6o9QKOMs6B3bzFXkOiPhf20,3274
|
|
22
|
+
miso_client/utils/filter.py,sha256=obkdQ3FFD69Qg0VviFMoM8y6zAP5JYBaIujmOT9diU8,8984
|
|
23
|
+
miso_client/utils/http_client.py,sha256=H386RsKjiw-JG7XhrTn8gCRDMWMvp8s7bC1jqa7qvGk,23807
|
|
24
|
+
miso_client/utils/internal_http_client.py,sha256=jgiaO94EiIUbMQWUKN4FhYqOQ9r0BZea0_grRcOepL4,16078
|
|
25
|
+
miso_client/utils/jwt_tools.py,sha256=-pvz5nk5BztEnhFnL-dtOv8Q5E0G2oh4RwFrVk2rVpg,1981
|
|
26
|
+
miso_client/utils/pagination.py,sha256=_sUzAGSCiUujVa9W3nedVRaCx18_9MuT_9bBlWmVc9o,4449
|
|
27
|
+
miso_client/utils/sensitive_fields_loader.py,sha256=EHODxyM1Gw7hgKXCvJ1B4Hf4LZqcEqWEXu4q5CPFaic,3667
|
|
28
|
+
miso_client/utils/sort.py,sha256=_bCLRCGevgNkXYqsgXasc3vaDQwkXV76kgHIyw-dato,3438
|
|
29
|
+
miso_client-0.5.0.dist-info/licenses/LICENSE,sha256=3hoU8LdT9_EIFIx6FjMk5sQnVCBMX3FRIOzqqy5im4c,1076
|
|
30
|
+
miso_client-0.5.0.dist-info/METADATA,sha256=ZzmZZNHxnv4n3P95ZJgMAPFKRE3gLTgYUhCG9sh20C4,28396
|
|
31
|
+
miso_client-0.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
32
|
+
miso_client-0.5.0.dist-info/top_level.txt,sha256=8i_FNeRn8PRy6scnXOpVr-IJYsArkqIvxRMTZPtik9E,12
|
|
33
|
+
miso_client-0.5.0.dist-info/RECORD,,
|