nc-user-terminator 0.1.3__tar.gz → 0.1.4__tar.gz

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 nc-user-terminator might be problematic. Click here for more details.

@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nc-user-terminator
3
- Version: 0.1.3
3
+ Version: 0.1.4
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
11
  Dynamic: description-content-type
11
12
  Dynamic: requires-python
@@ -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
+
@@ -1,12 +1,14 @@
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
10
  TENANT_HEADER = "X-Tenant-Code"
11
+ DEFAULT_TTL = 3600
10
12
 
11
13
 
12
14
  class OAuthClient:
@@ -16,6 +18,7 @@ class OAuthClient:
16
18
  tenant_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 客户端
@@ -29,6 +32,9 @@ class OAuthClient:
29
32
  self._tenant_code = tenant_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
  # 内部异步包装
@@ -143,19 +149,83 @@ 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
+ self._cache_user(token, res_dict)
153
213
  return UserResponse(res_dict)
154
214
 
155
215
  async def say_my_name_async(self, token: str) -> UserResponse:
216
+ if not self._async_cache:
217
+ raise RuntimeError("异步方法只支持异步缓存,请传入 async_cache")
218
+
156
219
  """异步获取当前用户"""
220
+ key = f"user:{token}"
221
+ data = await self._cache.get(key)
222
+ if data:
223
+ return UserResponse(data)
224
+
157
225
  headers = self._headers({"Authorization": f"Bearer {token}"})
158
226
  res_dict = await self._arequest("GET", f"{self._base_url}/api/me", headers=headers)
227
+
228
+ await self._cache_user_async(token, res_dict)
159
229
  return UserResponse(res_dict)
160
230
 
161
231
  # 刷新过期时间
@@ -178,9 +248,19 @@ class OAuthClient:
178
248
  # 登出
179
249
  def logout(self, token: str):
180
250
  headers = self._headers({"Authorization": f"Bearer {token}"})
181
- return request("POST", f"{self._base_url}/api/auth/logout", headers=headers)
251
+ resp = request("POST", f"{self._base_url}/api/auth/logout", headers=headers)
252
+
253
+ data = self._cache.get(f"user:{token}")
254
+ self._uncache_user(token, data)
255
+ return resp
256
+
182
257
 
183
258
  async def logout_async(self, token: str) -> dict:
184
259
  """异步获取当前用户"""
185
260
  headers = self._headers({"Authorization": f"Bearer {token}"})
186
- return await self._arequest("POST", f"{self._base_url}/api/auth/logout", headers=headers)
261
+ resp = await self._arequest("POST", f"{self._base_url}/api/auth/logout", headers=headers)
262
+
263
+ data = await self._cache.get(f"user:{token}")
264
+ await self._uncache_user_async(token, data)
265
+ return resp
266
+
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nc-user-terminator
3
- Version: 0.1.3
3
+ Version: 0.1.4
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
11
  Dynamic: description-content-type
11
12
  Dynamic: requires-python
@@ -2,6 +2,7 @@ README.md
2
2
  pyproject.toml
3
3
  setup.py
4
4
  nc_user_manager/__init__.py
5
+ nc_user_manager/cache.py
5
6
  nc_user_manager/client.py
6
7
  nc_user_manager/exceptions.py
7
8
  nc_user_manager/models.py
@@ -9,4 +10,5 @@ nc_user_manager/utils.py
9
10
  nc_user_terminator.egg-info/PKG-INFO
10
11
  nc_user_terminator.egg-info/SOURCES.txt
11
12
  nc_user_terminator.egg-info/dependency_links.txt
13
+ nc_user_terminator.egg-info/requires.txt
12
14
  nc_user_terminator.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ cachetools==6.2.0
@@ -4,10 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nc-user-terminator"
7
- version = "0.1.3"
7
+ version = "0.1.4"
8
8
  description = "OAuth client wrapper for multi-tenant projects"
9
9
  authors = [
10
10
  { name = "bw_song" }
11
11
  ]
12
12
  requires-python = ">=3.8"
13
- dependencies = []
13
+ dependencies = [
14
+ "cachetools==6.2.0"
15
+ ]