miso-client 0.1.0__py3-none-any.whl → 0.2.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.

@@ -8,6 +8,7 @@ Optimized to extract userId from JWT token before API calls for cache optimizati
8
8
 
9
9
  import time
10
10
  from typing import List, cast
11
+
11
12
  from ..models.config import PermissionResult
12
13
  from ..services.cache import CacheService
13
14
  from ..utils.http_client import HttpClient
@@ -16,11 +17,11 @@ from ..utils.jwt_tools import extract_user_id
16
17
 
17
18
  class PermissionService:
18
19
  """Permission service for user authorization with caching."""
19
-
20
+
20
21
  def __init__(self, http_client: HttpClient, cache: CacheService):
21
22
  """
22
23
  Initialize permission service.
23
-
24
+
24
25
  Args:
25
26
  http_client: HTTP client instance
26
27
  cache: Cache service instance (handles Redis + in-memory fallback)
@@ -33,12 +34,12 @@ class PermissionService:
33
34
  async def get_permissions(self, token: str) -> List[str]:
34
35
  """
35
36
  Get user permissions with Redis caching.
36
-
37
+
37
38
  Optimized to extract userId from token first to check cache before API call.
38
-
39
+
39
40
  Args:
40
41
  token: JWT token
41
-
42
+
42
43
  Returns:
43
44
  List of user permissions
44
45
  """
@@ -57,9 +58,7 @@ class PermissionService:
57
58
  # If we don't have userId, get it from validate endpoint
58
59
  if not user_id:
59
60
  user_info = await self.http_client.authenticated_request(
60
- "POST",
61
- "/api/auth/validate",
62
- token
61
+ "POST", "/api/auth/validate", token
63
62
  )
64
63
  user_id = user_info.get("user", {}).get("id") if user_info else None
65
64
  if not user_id:
@@ -68,9 +67,7 @@ class PermissionService:
68
67
 
69
68
  # Cache miss - fetch from controller
70
69
  permission_result = await self.http_client.authenticated_request(
71
- "GET",
72
- "/api/auth/permissions", # Backend knows app/env from client token
73
- token
70
+ "GET", "/api/auth/permissions", token # Backend knows app/env from client token
74
71
  )
75
72
 
76
73
  permission_data = PermissionResult(**permission_result)
@@ -81,11 +78,11 @@ class PermissionService:
81
78
  await self.cache.set(
82
79
  cache_key,
83
80
  {"permissions": permissions, "timestamp": int(time.time() * 1000)},
84
- self.permission_ttl
81
+ self.permission_ttl,
85
82
  )
86
83
 
87
84
  return permissions
88
-
85
+
89
86
  except Exception:
90
87
  # Failed to get permissions, return empty list
91
88
  return []
@@ -93,11 +90,11 @@ class PermissionService:
93
90
  async def has_permission(self, token: str, permission: str) -> bool:
94
91
  """
95
92
  Check if user has specific permission.
96
-
93
+
97
94
  Args:
98
95
  token: JWT token
99
96
  permission: Permission to check
100
-
97
+
101
98
  Returns:
102
99
  True if user has the permission, False otherwise
103
100
  """
@@ -107,11 +104,11 @@ class PermissionService:
107
104
  async def has_any_permission(self, token: str, permissions: List[str]) -> bool:
108
105
  """
109
106
  Check if user has any of the specified permissions.
110
-
107
+
111
108
  Args:
112
109
  token: JWT token
113
110
  permissions: List of permissions to check
114
-
111
+
115
112
  Returns:
116
113
  True if user has any of the permissions, False otherwise
117
114
  """
@@ -121,11 +118,11 @@ class PermissionService:
121
118
  async def has_all_permissions(self, token: str, permissions: List[str]) -> bool:
122
119
  """
123
120
  Check if user has all of the specified permissions.
124
-
121
+
125
122
  Args:
126
123
  token: JWT token
127
124
  permissions: List of permissions to check
128
-
125
+
129
126
  Returns:
130
127
  True if user has all permissions, False otherwise
131
128
  """
@@ -135,21 +132,19 @@ class PermissionService:
135
132
  async def refresh_permissions(self, token: str) -> List[str]:
136
133
  """
137
134
  Force refresh permissions from controller (bypass cache).
138
-
135
+
139
136
  Args:
140
137
  token: JWT token
141
-
138
+
142
139
  Returns:
143
140
  Fresh list of user permissions
144
141
  """
145
142
  try:
146
143
  # Get user info to extract userId
147
144
  user_info = await self.http_client.authenticated_request(
148
- "POST",
149
- "/api/auth/validate",
150
- token
145
+ "POST", "/api/auth/validate", token
151
146
  )
152
-
147
+
153
148
  user_id = user_info.get("user", {}).get("id") if user_info else None
154
149
  if not user_id:
155
150
  return []
@@ -158,9 +153,7 @@ class PermissionService:
158
153
 
159
154
  # Fetch fresh permissions from controller using refresh endpoint
160
155
  permission_result = await self.http_client.authenticated_request(
161
- "GET",
162
- "/api/auth/permissions/refresh",
163
- token
156
+ "GET", "/api/auth/permissions/refresh", token
164
157
  )
165
158
 
166
159
  permission_data = PermissionResult(**permission_result)
@@ -170,11 +163,11 @@ class PermissionService:
170
163
  await self.cache.set(
171
164
  cache_key,
172
165
  {"permissions": permissions, "timestamp": int(time.time() * 1000)},
173
- self.permission_ttl
166
+ self.permission_ttl,
174
167
  )
175
168
 
176
169
  return permissions
177
-
170
+
178
171
  except Exception:
179
172
  # Failed to refresh permissions, return empty list
180
173
  return []
@@ -182,18 +175,16 @@ class PermissionService:
182
175
  async def clear_permissions_cache(self, token: str) -> None:
183
176
  """
184
177
  Clear cached permissions for a user.
185
-
178
+
186
179
  Args:
187
180
  token: JWT token
188
181
  """
189
182
  try:
190
183
  # Get user info to extract userId
191
184
  user_info = await self.http_client.authenticated_request(
192
- "POST",
193
- "/api/auth/validate",
194
- token
185
+ "POST", "/api/auth/validate", token
195
186
  )
196
-
187
+
197
188
  user_id = user_info.get("user", {}).get("id") if user_info else None
198
189
  if not user_id:
199
190
  return
@@ -202,7 +193,7 @@ class PermissionService:
202
193
 
203
194
  # Clear from cache (CacheService handles Redis + in-memory automatically)
204
195
  await self.cache.delete(cache_key)
205
-
196
+
206
197
  except Exception:
207
198
  # Failed to clear cache, silently continue
208
199
  pass
@@ -5,18 +5,20 @@ This module provides Redis connectivity with graceful degradation when Redis
5
5
  is unavailable. It handles caching of roles and permissions, and log queuing.
6
6
  """
7
7
 
8
- import redis.asyncio as redis
9
8
  from typing import Optional
9
+
10
+ import redis.asyncio as redis
11
+
10
12
  from ..models.config import RedisConfig
11
13
 
12
14
 
13
15
  class RedisService:
14
16
  """Redis service for caching and log queuing."""
15
-
17
+
16
18
  def __init__(self, config: Optional[RedisConfig] = None):
17
19
  """
18
20
  Initialize Redis service.
19
-
21
+
20
22
  Args:
21
23
  config: Optional Redis configuration
22
24
  """
@@ -27,7 +29,7 @@ class RedisService:
27
29
  async def connect(self) -> None:
28
30
  """
29
31
  Connect to Redis.
30
-
32
+
31
33
  Raises:
32
34
  Exception: If connection fails and config is provided
33
35
  """
@@ -46,7 +48,7 @@ class RedisService:
46
48
  socket_connect_timeout=5,
47
49
  socket_timeout=5,
48
50
  )
49
-
51
+
50
52
  # Test connection
51
53
  # Some redis stubs type ping as possibly non-awaitable; support both
52
54
  resp = self.redis.ping()
@@ -54,7 +56,7 @@ class RedisService:
54
56
  await resp # type: ignore[misc]
55
57
  self.connected = True
56
58
  print("Connected to Redis")
57
-
59
+
58
60
  except Exception as error:
59
61
  print(f"Failed to connect to Redis: {error}")
60
62
  self.connected = False
@@ -71,7 +73,7 @@ class RedisService:
71
73
  def is_connected(self) -> bool:
72
74
  """
73
75
  Check if Redis is connected.
74
-
76
+
75
77
  Returns:
76
78
  True if connected, False otherwise
77
79
  """
@@ -80,10 +82,10 @@ class RedisService:
80
82
  async def get(self, key: str) -> Optional[str]:
81
83
  """
82
84
  Get value from Redis.
83
-
85
+
84
86
  Args:
85
87
  key: Redis key
86
-
88
+
87
89
  Returns:
88
90
  Value if found, None otherwise
89
91
  """
@@ -106,12 +108,12 @@ class RedisService:
106
108
  async def set(self, key: str, value: str, ttl: int) -> bool:
107
109
  """
108
110
  Set value in Redis with TTL.
109
-
111
+
110
112
  Args:
111
113
  key: Redis key
112
114
  value: Value to store
113
115
  ttl: Time to live in seconds
114
-
116
+
115
117
  Returns:
116
118
  True if successful, False otherwise
117
119
  """
@@ -132,10 +134,10 @@ class RedisService:
132
134
  async def delete(self, key: str) -> bool:
133
135
  """
134
136
  Delete key from Redis.
135
-
137
+
136
138
  Args:
137
139
  key: Redis key
138
-
140
+
139
141
  Returns:
140
142
  True if successful, False otherwise
141
143
  """
@@ -156,11 +158,11 @@ class RedisService:
156
158
  async def rpush(self, queue: str, value: str) -> bool:
157
159
  """
158
160
  Push value to Redis list (for log queuing).
159
-
161
+
160
162
  Args:
161
163
  queue: Queue name
162
164
  value: Value to push
163
-
165
+
164
166
  Returns:
165
167
  True if successful, False otherwise
166
168
  """
@@ -8,6 +8,7 @@ Optimized to extract userId from JWT token before API calls for cache optimizati
8
8
 
9
9
  import time
10
10
  from typing import List, cast
11
+
11
12
  from ..models.config import RoleResult
12
13
  from ..services.cache import CacheService
13
14
  from ..utils.http_client import HttpClient
@@ -16,11 +17,11 @@ from ..utils.jwt_tools import extract_user_id
16
17
 
17
18
  class RoleService:
18
19
  """Role service for user authorization with caching."""
19
-
20
+
20
21
  def __init__(self, http_client: HttpClient, cache: CacheService):
21
22
  """
22
23
  Initialize role service.
23
-
24
+
24
25
  Args:
25
26
  http_client: HTTP client instance
26
27
  cache: Cache service instance (handles Redis + in-memory fallback)
@@ -33,12 +34,12 @@ class RoleService:
33
34
  async def get_roles(self, token: str) -> List[str]:
34
35
  """
35
36
  Get user roles with Redis caching.
36
-
37
+
37
38
  Optimized to extract userId from token first to check cache before API call.
38
-
39
+
39
40
  Args:
40
41
  token: JWT token
41
-
42
+
42
43
  Returns:
43
44
  List of user roles
44
45
  """
@@ -57,9 +58,7 @@ class RoleService:
57
58
  # If we don't have userId, get it from validate endpoint
58
59
  if not user_id:
59
60
  user_info = await self.http_client.authenticated_request(
60
- "POST",
61
- "/api/auth/validate",
62
- token
61
+ "POST", "/api/auth/validate", token
63
62
  )
64
63
  user_id = user_info.get("user", {}).get("id") if user_info else None
65
64
  if not user_id:
@@ -68,24 +67,20 @@ class RoleService:
68
67
 
69
68
  # Cache miss - fetch from controller
70
69
  role_result = await self.http_client.authenticated_request(
71
- "GET",
72
- "/api/auth/roles", # Backend knows app/env from client token
73
- token
70
+ "GET", "/api/auth/roles", token # Backend knows app/env from client token
74
71
  )
75
-
72
+
76
73
  role_data = RoleResult(**role_result)
77
74
  roles = role_data.roles or []
78
75
 
79
76
  # Cache the result (CacheService handles Redis + in-memory automatically)
80
77
  assert cache_key is not None
81
78
  await self.cache.set(
82
- cache_key,
83
- {"roles": roles, "timestamp": int(time.time() * 1000)},
84
- self.role_ttl
79
+ cache_key, {"roles": roles, "timestamp": int(time.time() * 1000)}, self.role_ttl
85
80
  )
86
81
 
87
82
  return roles
88
-
83
+
89
84
  except Exception:
90
85
  # Failed to get roles, return empty list
91
86
  return []
@@ -93,11 +88,11 @@ class RoleService:
93
88
  async def has_role(self, token: str, role: str) -> bool:
94
89
  """
95
90
  Check if user has specific role.
96
-
91
+
97
92
  Args:
98
93
  token: JWT token
99
94
  role: Role to check
100
-
95
+
101
96
  Returns:
102
97
  True if user has the role, False otherwise
103
98
  """
@@ -107,11 +102,11 @@ class RoleService:
107
102
  async def has_any_role(self, token: str, roles: List[str]) -> bool:
108
103
  """
109
104
  Check if user has any of the specified roles.
110
-
105
+
111
106
  Args:
112
107
  token: JWT token
113
108
  roles: List of roles to check
114
-
109
+
115
110
  Returns:
116
111
  True if user has any of the roles, False otherwise
117
112
  """
@@ -121,11 +116,11 @@ class RoleService:
121
116
  async def has_all_roles(self, token: str, roles: List[str]) -> bool:
122
117
  """
123
118
  Check if user has all of the specified roles.
124
-
119
+
125
120
  Args:
126
121
  token: JWT token
127
122
  roles: List of roles to check
128
-
123
+
129
124
  Returns:
130
125
  True if user has all roles, False otherwise
131
126
  """
@@ -135,21 +130,19 @@ class RoleService:
135
130
  async def refresh_roles(self, token: str) -> List[str]:
136
131
  """
137
132
  Force refresh roles from controller (bypass cache).
138
-
133
+
139
134
  Args:
140
135
  token: JWT token
141
-
136
+
142
137
  Returns:
143
138
  Fresh list of user roles
144
139
  """
145
140
  try:
146
141
  # Get user info to extract userId
147
142
  user_info = await self.http_client.authenticated_request(
148
- "POST",
149
- "/api/auth/validate",
150
- token
143
+ "POST", "/api/auth/validate", token
151
144
  )
152
-
145
+
153
146
  user_id = user_info.get("user", {}).get("id") if user_info else None
154
147
  if not user_id:
155
148
  return []
@@ -158,23 +151,19 @@ class RoleService:
158
151
 
159
152
  # Fetch fresh roles from controller using refresh endpoint
160
153
  role_result = await self.http_client.authenticated_request(
161
- "GET",
162
- "/api/auth/roles/refresh",
163
- token
154
+ "GET", "/api/auth/roles/refresh", token
164
155
  )
165
-
156
+
166
157
  role_data = RoleResult(**role_result)
167
158
  roles = role_data.roles or []
168
159
 
169
160
  # Update cache with fresh data (CacheService handles Redis + in-memory automatically)
170
161
  await self.cache.set(
171
- cache_key,
172
- {"roles": roles, "timestamp": int(time.time() * 1000)},
173
- self.role_ttl
162
+ cache_key, {"roles": roles, "timestamp": int(time.time() * 1000)}, self.role_ttl
174
163
  )
175
164
 
176
165
  return roles
177
-
166
+
178
167
  except Exception:
179
168
  # Failed to refresh roles, return empty list
180
169
  return []
@@ -1,9 +1,9 @@
1
1
  """Utility modules for MisoClient SDK."""
2
2
 
3
- from .http_client import HttpClient
4
3
  from .config_loader import load_config
5
4
  from .data_masker import DataMasker
6
- from .jwt_tools import decode_token, extract_user_id, extract_session_id
5
+ from .http_client import HttpClient
6
+ from .jwt_tools import decode_token, extract_session_id, extract_user_id
7
7
 
8
8
  __all__ = [
9
9
  "HttpClient",
@@ -12,4 +12,4 @@ __all__ = [
12
12
  "decode_token",
13
13
  "extract_user_id",
14
14
  "extract_session_id",
15
- ]
15
+ ]
@@ -6,50 +6,55 @@ Automatically loads environment variables with sensible defaults.
6
6
 
7
7
  import os
8
8
  from typing import Literal, cast
9
- from ..models.config import MisoClientConfig, RedisConfig
9
+
10
10
  from ..errors import ConfigurationError
11
+ from ..models.config import MisoClientConfig, RedisConfig
11
12
 
12
13
 
13
14
  def load_config() -> MisoClientConfig:
14
15
  """
15
16
  Load configuration from environment variables with defaults.
16
-
17
+
17
18
  Required environment variables:
18
19
  - MISO_CONTROLLER_URL (or default to https://controller.aifabrix.ai)
19
20
  - MISO_CLIENTID or MISO_CLIENT_ID
20
21
  - MISO_CLIENTSECRET or MISO_CLIENT_SECRET
21
-
22
+
22
23
  Optional environment variables:
23
24
  - MISO_LOG_LEVEL (debug, info, warn, error)
25
+ - API_KEY (for testing - bypasses OAuth2 authentication)
24
26
  - REDIS_HOST (if Redis is used)
25
27
  - REDIS_PORT (default: 6379)
26
28
  - REDIS_PASSWORD
27
29
  - REDIS_DB (default: 0)
28
30
  - REDIS_KEY_PREFIX (default: miso:)
29
-
31
+
30
32
  Returns:
31
33
  MisoClientConfig instance
32
-
34
+
33
35
  Raises:
34
36
  ConfigurationError: If required environment variables are missing
35
37
  """
36
38
  # Load dotenv if available (similar to TypeScript dotenv/config)
37
39
  try:
38
40
  from dotenv import load_dotenv
41
+
39
42
  load_dotenv()
40
43
  except ImportError:
41
44
  pass # dotenv not installed, continue without it
42
-
45
+
43
46
  controller_url = os.environ.get("MISO_CONTROLLER_URL") or "https://controller.aifabrix.ai"
44
-
47
+
45
48
  client_id = os.environ.get("MISO_CLIENTID") or os.environ.get("MISO_CLIENT_ID") or ""
46
49
  if not client_id:
47
50
  raise ConfigurationError("MISO_CLIENTID environment variable is required")
48
-
49
- client_secret = os.environ.get("MISO_CLIENTSECRET") or os.environ.get("MISO_CLIENT_SECRET") or ""
51
+
52
+ client_secret = (
53
+ os.environ.get("MISO_CLIENTSECRET") or os.environ.get("MISO_CLIENT_SECRET") or ""
54
+ )
50
55
  if not client_secret:
51
56
  raise ConfigurationError("MISO_CLIENTSECRET environment variable is required")
52
-
57
+
53
58
  log_level_str = os.environ.get("MISO_LOG_LEVEL", "info")
54
59
  if log_level_str not in ["debug", "info", "warn", "error"]:
55
60
  log_level_str = "info"
@@ -57,14 +62,18 @@ def load_config() -> MisoClientConfig:
57
62
  log_level: Literal["debug", "info", "warn", "error"] = cast(
58
63
  Literal["debug", "info", "warn", "error"], log_level_str
59
64
  )
60
-
65
+
66
+ # Optional API_KEY for testing
67
+ api_key = os.environ.get("API_KEY")
68
+
61
69
  config: MisoClientConfig = MisoClientConfig(
62
70
  controller_url=controller_url,
63
71
  client_id=client_id,
64
72
  client_secret=client_secret,
65
73
  log_level=log_level,
74
+ api_key=api_key,
66
75
  )
67
-
76
+
68
77
  # Optional Redis configuration
69
78
  redis_host = os.environ.get("REDIS_HOST")
70
79
  if redis_host:
@@ -72,7 +81,7 @@ def load_config() -> MisoClientConfig:
72
81
  redis_password = os.environ.get("REDIS_PASSWORD")
73
82
  redis_db = int(os.environ.get("REDIS_DB", "0")) if os.environ.get("REDIS_DB") else 0
74
83
  redis_key_prefix = os.environ.get("REDIS_KEY_PREFIX", "miso:")
75
-
84
+
76
85
  redis_config = RedisConfig(
77
86
  host=redis_host,
78
87
  port=redis_port,
@@ -80,8 +89,7 @@ def load_config() -> MisoClientConfig:
80
89
  db=redis_db,
81
90
  key_prefix=redis_key_prefix,
82
91
  )
83
-
92
+
84
93
  config.redis = redis_config
85
-
86
- return config
87
94
 
95
+ return config