nc-user-terminator 0.1.3__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.
@@ -0,0 +1,88 @@
1
+ import json
2
+ from typing import Any, Optional
3
+ from cachetools import TTLCache
4
+
5
+
6
+ DEFAULT_TTL = 3600
7
+
8
+
9
+ class BaseCache:
10
+ """缓存接口"""
11
+ is_async = False
12
+
13
+ def set(self, key: str, value: Any, ttl: Optional[int] = None):
14
+ raise NotImplementedError
15
+
16
+ def get(self, key: str) -> Any:
17
+ raise NotImplementedError
18
+
19
+ def delete(self, key: str):
20
+ raise NotImplementedError
21
+
22
+
23
+ # ---------------------
24
+ # 内存缓存
25
+ # ---------------------
26
+ class MemoryCache(BaseCache):
27
+ is_async = False
28
+
29
+ def __init__(self, maxsize: int = 1000, ttl: int = DEFAULT_TTL):
30
+ self._cache = TTLCache(maxsize=maxsize, ttl=ttl)
31
+
32
+ def set(self, key: str, value: Any, ttl: Optional[int] = None):
33
+ # cachetools TTLCache 不支持 per-key TTL, 所以只使用全局 TTL
34
+ self._cache[key] = value
35
+
36
+ def get(self, key: str) -> Any:
37
+ return self._cache.get(key)
38
+
39
+ def delete(self, key: str):
40
+ self._cache.pop(key, None)
41
+
42
+
43
+ # ---------------------
44
+ # Redis 缓存
45
+ # ---------------------
46
+ class RedisCache(BaseCache):
47
+ """同步 Redis"""
48
+ is_async = False
49
+
50
+ def __init__(self, redis_client, ttl: int = DEFAULT_TTL):
51
+ self.redis = redis_client
52
+ self.ttl = ttl
53
+
54
+ def set(self, key: str, value: Any, ttl: Optional[int] = None):
55
+ ttl = ttl or self.ttl
56
+ self.redis.setex(key, ttl, json.dumps(value))
57
+
58
+ def get(self, key: str) -> Any:
59
+ data = self.redis.get(key)
60
+ if data:
61
+ return json.loads(data)
62
+ return None
63
+
64
+ def delete(self, key: str):
65
+ self.redis.delete(key)
66
+
67
+
68
+ class AsyncRedisCache(BaseCache):
69
+ """异步 Redis"""
70
+ is_async = True
71
+
72
+ def __init__(self, redis_client, ttl: int = DEFAULT_TTL):
73
+ self.redis = redis_client
74
+ self.ttl = ttl
75
+
76
+ async def set(self, key: str, value: Any, ttl: Optional[int] = None):
77
+ ttl = ttl or self.ttl
78
+ await self.redis.setex(key, ttl, json.dumps(value))
79
+
80
+ async def get(self, key: str) -> Any:
81
+ data = await self.redis.get(key)
82
+ if data:
83
+ return json.loads(data)
84
+ return None
85
+
86
+ async def delete(self, key: str):
87
+ await self.redis.delete(key)
88
+
nc_user_manager/client.py CHANGED
@@ -1,34 +1,40 @@
1
1
  import asyncio
2
2
  from typing import Optional, Dict, Any
3
3
 
4
+ from cache import MemoryCache, BaseCache, AsyncRedisCache
4
5
  from models import UserResponse, OAuth2AuthorizeResponse, CallbackResponse
5
6
  from exceptions import OAuthError
6
7
  from utils import request
7
8
 
8
9
 
9
- TENANT_HEADER = "X-Tenant-Code"
10
+ PRODUCT_HEADER = "X-Product-Code"
11
+ DEFAULT_TTL = 3600
10
12
 
11
13
 
12
14
  class OAuthClient:
13
15
  def __init__(
14
16
  self,
15
17
  base_url: str,
16
- tenant_code: str,
18
+ product_code: str,
17
19
  redirect_url: Optional[str] = None,
18
20
  single_session: bool = False,
21
+ cache: Optional[BaseCache] = None,
19
22
  ):
20
23
  """
21
24
  OAuth 客户端
22
25
 
23
26
  :param base_url: 服务端基础地址 (例如 http://localhost:8000)
24
- :param tenant_code: 租户编码
27
+ :param product_code: 产品编码
25
28
  :param redirect_url: 可选,重定向地址
26
29
  :param single_session: 是否单会话登录
27
30
  """
28
31
  self._base_url = base_url.rstrip("/")
29
- self._tenant_code = tenant_code
32
+ self._product_code = product_code
30
33
  self._redirect_url = redirect_url
31
34
  self._single_session = single_session
35
+ self._cache = cache or MemoryCache(maxsize=10000, ttl=DEFAULT_TTL)
36
+ # 异步缓存,仅允许异步 redis
37
+ self._async_cache = self._cache.is_async
32
38
 
33
39
  # ----------------------
34
40
  # 内部异步包装
@@ -37,7 +43,7 @@ class OAuthClient:
37
43
  return await asyncio.to_thread(request, *args, **kwargs)
38
44
 
39
45
  def _headers(self, extra: Optional[Dict[str, str]] = None) -> Dict[str, str]:
40
- headers = {TENANT_HEADER: self._tenant_code}
46
+ headers = {PRODUCT_HEADER: self._product_code}
41
47
  if extra:
42
48
  headers.update(extra)
43
49
  return headers
@@ -143,19 +149,85 @@ class OAuthClient:
143
149
  )
144
150
  return CallbackResponse(res_dict)
145
151
 
152
+ # ----------------------
153
+ # 缓存工具
154
+ # ----------------------
155
+ def _cache_user(self, token: str, user_dict: dict, ttl=DEFAULT_TTL):
156
+ key = f"user:{token}"
157
+ self._cache.set(key, user_dict, ttl)
158
+
159
+ if self._single_session:
160
+ user_id = user_dict.get("id")
161
+ if user_id:
162
+ old_token = self._cache.get(f"user_token:{user_id}")
163
+ if old_token and old_token != token:
164
+ self._cache.delete(f"user:{old_token}")
165
+ self._cache.set(f"user_token:{user_id}", token, ttl)
166
+
167
+ async def _cache_user_async(self, token: str, user_dict: dict, ttl=DEFAULT_TTL):
168
+ if not self._async_cache:
169
+ raise RuntimeError("异步缓存未配置")
170
+
171
+ key = f"user:{token}"
172
+ await self._cache.set(key, user_dict, ttl)
173
+
174
+ if self._single_session:
175
+ user_id = user_dict.get("id")
176
+ if user_id:
177
+ old_token = await self._cache.get(f"user_token:{user_id}")
178
+ if old_token and old_token != token:
179
+ await self._cache.delete(f"user:{old_token}")
180
+ await self._cache.set(f"user_token:{user_id}", token, ttl)
181
+
182
+ def _uncache_user(self, token: str, user_dict: Optional[dict] = None):
183
+ self._cache.delete(f"user:{token}")
184
+ if self._single_session and user_dict:
185
+ user_id = user_dict.get("id")
186
+ if user_id:
187
+ self._cache.delete(f"user_token:{user_id}")
188
+
189
+ async def _uncache_user_async(self, token: str, user_dict: Optional[dict] = None):
190
+ await self._cache.delete(f"user:{token}")
191
+ if self._single_session and user_dict:
192
+ user_id = user_dict.get("id")
193
+ if user_id:
194
+ await self._cache.delete(f"user_token:{user_id}")
195
+
146
196
  # ----------------------
147
197
  # 获取用户信息
148
198
  # ----------------------
149
199
  def say_my_name(self, token: str) -> UserResponse:
150
200
  """同步获取当前用户"""
201
+ if self._async_cache:
202
+ raise RuntimeError("同步缓存未配置")
203
+
204
+ key = f"user:{token}"
205
+ data = self._cache.get(key)
206
+ if data:
207
+ return UserResponse(data)
208
+
151
209
  headers = self._headers({"Authorization": f"Bearer {token}"})
152
210
  res_dict = request("GET", f"{self._base_url}/api/me", headers=headers)
211
+
212
+ if res_dict.get("success", True):
213
+ self._cache_user(token, res_dict)
153
214
  return UserResponse(res_dict)
154
215
 
155
216
  async def say_my_name_async(self, token: str) -> UserResponse:
217
+ if not self._async_cache:
218
+ raise RuntimeError("异步方法只支持异步缓存,请传入 async_cache")
219
+
156
220
  """异步获取当前用户"""
221
+ key = f"user:{token}"
222
+ data = await self._cache.get(key)
223
+ if data:
224
+ return UserResponse(data)
225
+
157
226
  headers = self._headers({"Authorization": f"Bearer {token}"})
158
227
  res_dict = await self._arequest("GET", f"{self._base_url}/api/me", headers=headers)
228
+ if res_dict.get("success", True):
229
+ await self._cache_user_async(token, res_dict)
230
+
159
231
  return UserResponse(res_dict)
160
232
 
161
233
  # 刷新过期时间
@@ -178,9 +250,46 @@ class OAuthClient:
178
250
  # 登出
179
251
  def logout(self, token: str):
180
252
  headers = self._headers({"Authorization": f"Bearer {token}"})
181
- return request("POST", f"{self._base_url}/api/auth/logout", headers=headers)
253
+ resp = request("POST", f"{self._base_url}/api/auth/logout", headers=headers)
254
+
255
+ data = self._cache.get(f"user:{token}")
256
+ self._uncache_user(token, data)
257
+ return resp
258
+
182
259
 
183
260
  async def logout_async(self, token: str) -> dict:
184
261
  """异步获取当前用户"""
185
262
  headers = self._headers({"Authorization": f"Bearer {token}"})
186
- return await self._arequest("POST", f"{self._base_url}/api/auth/logout", headers=headers)
263
+ resp = await self._arequest("POST", f"{self._base_url}/api/auth/logout", headers=headers)
264
+
265
+ data = await self._cache.get(f"user:{token}")
266
+ await self._uncache_user_async(token, data)
267
+ return resp
268
+
269
+ async def main():
270
+ def get_redis_url() -> str:
271
+ return f'redis://default:1q2w3e@localhost:6379/1'
272
+
273
+
274
+ import redis.asyncio as redis
275
+ # —— Redis 连接与策略(Bearer + Redis)——
276
+ _redis = redis.from_url(get_redis_url(), decode_responses=True)
277
+
278
+ redis_cache = AsyncRedisCache(_redis)
279
+
280
+ client = OAuthClient("http://localhost:8000/", "bankgpt", "http://localhost:8000/auth/google/custom_callback", single_session=True, cache=redis_cache)
281
+ # authorize = client.authorize("google")
282
+ # print(authorize)
283
+
284
+
285
+ name = await client.say_my_name_async("Tu72u4SVue4ezadi1v5Ui9r9HjxH7YqbkY_yf5jFtPQ")
286
+ print(name)
287
+ name = await client.say_my_name_async("Tu72u4SVue4ezadi1v5Ui9r9HjxH7YqbkY_yf5jFtPQ")
288
+ print(name)
289
+ name = await client.logout_async("Tu72u4SVue4ezadi1v5Ui9r9HjxH7YqbkY_yf5jFtPQ")
290
+ print(name)
291
+
292
+
293
+ if __name__ == '__main__':
294
+ asyncio.run(main())
295
+
nc_user_manager/utils.py CHANGED
@@ -2,11 +2,26 @@ import json
2
2
  import urllib.parse
3
3
  import urllib.request
4
4
  import urllib.error
5
+ from typing import Optional, Dict, Any
5
6
 
6
- from exceptions import OAuthError
7
7
 
8
-
9
- def request(method: str, url: str, params=None, headers=None, json_body=None) -> dict:
8
+ def request(
9
+ method: str,
10
+ url: str,
11
+ params: Optional[Dict[str, Any]] = None,
12
+ headers: Optional[Dict[str, str]] = None,
13
+ json_body: Optional[dict] = None
14
+ ) -> dict:
15
+ """
16
+ 发送 HTTP 请求并返回统一结果,不抛异常
17
+ 返回格式:
18
+ {
19
+ "status": int, # HTTP 状态码
20
+ "success": bool, # 是否成功 (2xx)
21
+ "body": dict or str, # JSON解析后的body, 或原始body
22
+ "error": Optional[str] # 错误信息
23
+ }
24
+ """
10
25
  # 拼接 GET 参数
11
26
  if params:
12
27
  query = urllib.parse.urlencode(params)
@@ -20,20 +35,43 @@ def request(method: str, url: str, params=None, headers=None, json_body=None) ->
20
35
  headers["Content-Type"] = "application/json"
21
36
 
22
37
  req = urllib.request.Request(url, method=method.upper(), data=data)
23
-
24
38
  if headers:
25
39
  for k, v in headers.items():
26
40
  req.add_header(k, v)
27
41
 
42
+ result = {
43
+ "status": 0,
44
+ "success": False,
45
+ "message": None,
46
+ "error": None
47
+ }
48
+
28
49
  try:
29
50
  with urllib.request.urlopen(req) as resp:
30
51
  body = resp.read().decode()
52
+ if not body:
53
+ return {}
31
54
  try:
32
55
  return json.loads(body)
33
56
  except json.JSONDecodeError:
34
- raise OAuthError("Invalid JSON response", resp.getcode(), body)
57
+ result["status"] = 200
58
+ result["success"] = False
59
+ result["message"] = body
60
+ result["error"] = "Invalid JSON response"
61
+ return result
35
62
  except urllib.error.HTTPError as e:
36
63
  body = e.read().decode() if e.fp else None
37
- raise OAuthError("HTTP request failed", e.code, body)
64
+ result["status"] = e.code
65
+ try:
66
+ result["message"] = json.loads(body) if body else None
67
+ except json.JSONDecodeError:
68
+ result["message"] = body
69
+ result["error"] = "HTTPError"
70
+ result["success"] = False
38
71
  except urllib.error.URLError as e:
39
- raise OAuthError(f"Network error: {e.reason}")
72
+ result["status"] = 0
73
+ result["message"] = None
74
+ result["error"] = f"NetworkError: {e.reason}"
75
+ result["success"] = False
76
+
77
+ return result
@@ -1,11 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nc-user-terminator
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: OAuth client wrapper for multi-tenant projects
5
5
  Author: bw_song
6
6
  Author-email: m132777096902@gmail.com
7
7
  Requires-Python: >=3.8
8
8
  Description-Content-Type: text/markdown
9
+ Requires-Dist: cachetools==6.2.0
9
10
  Dynamic: author-email
10
- Dynamic: description-content-type
11
11
  Dynamic: requires-python
12
+
13
+ # 更新
14
+ + V0.1.5
15
+ - 新增本地缓存机制
@@ -0,0 +1,10 @@
1
+ nc_user_manager/__init__.py,sha256=fF3FZD0XUW5YCfTbBeJs3RK-Mnm3IQ7lns6Eb_tMqPU,238
2
+ nc_user_manager/cache.py,sha256=u9ioXDwHmEJHRB4VKI4JU_pp-8Y5O4bLPm8-pwqiMVc,2273
3
+ nc_user_manager/client.py,sha256=71pQ_4YZy1f_BJ9Pc2Md1r57SStqI1qRxnXU9IGAm5Y,11276
4
+ nc_user_manager/exceptions.py,sha256=yUMDrh1HHZF36UUadQNHvJlDgEYSYoYOObs8Q11fjrE,351
5
+ nc_user_manager/models.py,sha256=mDK7zskIcaThxvUUTWVf6eMasw7YaA3hDGVVSd1ZJgo,1088
6
+ nc_user_manager/utils.py,sha256=gxFSaUq0oiymIlvHITu2L7EkU2ZYQmW2q_okmUxGBeU,2319
7
+ nc_user_terminator-0.1.5.dist-info/METADATA,sha256=1uCPVZ-m6nHokYebpNHDAdgJbtNxxJxugWNvzmqxm1U,378
8
+ nc_user_terminator-0.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ nc_user_terminator-0.1.5.dist-info/top_level.txt,sha256=kOAUtl6RYo-x3vMJL8It3KCJLoIFPvMUiAAyXjPQTYA,16
10
+ nc_user_terminator-0.1.5.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- nc_user_manager/__init__.py,sha256=fF3FZD0XUW5YCfTbBeJs3RK-Mnm3IQ7lns6Eb_tMqPU,238
2
- nc_user_manager/client.py,sha256=rGHrFv8ULTp_UXxg8BF-NAmbov9YUgzytJs6Nut7KrE,7254
3
- nc_user_manager/exceptions.py,sha256=yUMDrh1HHZF36UUadQNHvJlDgEYSYoYOObs8Q11fjrE,351
4
- nc_user_manager/models.py,sha256=mDK7zskIcaThxvUUTWVf6eMasw7YaA3hDGVVSd1ZJgo,1088
5
- nc_user_manager/utils.py,sha256=0QmJ9s3mzuYQSlwkcS8BW5XCe23lcoKskEeuqfdZHck,1245
6
- nc_user_terminator-0.1.3.dist-info/METADATA,sha256=YsxkK7xUovwQCna9nCZyjT5gkYJdVabM01ZmJag9t2g,327
7
- nc_user_terminator-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- nc_user_terminator-0.1.3.dist-info/top_level.txt,sha256=kOAUtl6RYo-x3vMJL8It3KCJLoIFPvMUiAAyXjPQTYA,16
9
- nc_user_terminator-0.1.3.dist-info/RECORD,,