the37lab-authlib 0.1.1755164205__py3-none-any.whl → 0.1.1756371198__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 the37lab-authlib might be problematic. Click here for more details.

the37lab_authlib/auth.py CHANGED
@@ -12,13 +12,21 @@ import logging
12
12
  import os
13
13
  from functools import wraps
14
14
  from isodate import parse_duration
15
+ import threading
16
+ import time
15
17
 
16
18
  logging.basicConfig(level=logging.DEBUG)
17
19
  logger = logging.getLogger(__name__)
18
20
 
19
21
  class AuthManager:
20
- def __init__(self, app=None, db_dsn=None, jwt_secret=None, oauth_config=None, id_type='integer', environment_prefix=None, api_tokens=None):
22
+ def __init__(self, app=None, db_dsn=None, jwt_secret=None, oauth_config=None, id_type='integer', environment_prefix=None, api_tokens=None, cache_ttl=10):
21
23
  self.user_override = None
24
+ self._user_cache = {}
25
+ self._cache_ttl = cache_ttl or 10 # 10 seconds
26
+ self._last_used_updates = {} # Track pending updates
27
+ self._update_lock = threading.Lock()
28
+ self._update_thread = None
29
+ self._shutdown_event = threading.Event()
22
30
  if environment_prefix:
23
31
  prefix = environment_prefix.upper() + '_'
24
32
  db_dsn = os.getenv(f'{prefix}DATABASE_URL')
@@ -65,6 +73,9 @@ class AuthManager:
65
73
 
66
74
  if app:
67
75
  self.init_app(app)
76
+
77
+ # Start the background update thread
78
+ self._start_update_thread()
68
79
 
69
80
  def _extract_token_from_header(self):
70
81
  auth = request.authorization
@@ -96,17 +107,34 @@ class AuthManager:
96
107
  }
97
108
  try:
98
109
  parsed = ApiToken.parse_token(api_token)
110
+
111
+ # Check cache first
112
+ cache_key = f"api_token_{parsed['id']}"
113
+ current_time = datetime.utcnow()
114
+
115
+ if cache_key in self._user_cache:
116
+ cached_data, cache_time = self._user_cache[cache_key]
117
+ if (current_time - cache_time).total_seconds() < self._cache_ttl:
118
+ logger.debug(f"Returning cached API token data for ID: {parsed['id']}")
119
+ return cached_data.copy() # Return a copy to avoid modifying cache
120
+
121
+ # Cache miss or expired, fetch from database
99
122
  with self.db.get_cursor() as cur:
100
123
  # First get the API token record
101
124
  cur.execute("""
102
- SELECT t.*, u.* FROM api_tokens t
125
+ SELECT t.*, u.*, r.name as role_name FROM api_tokens t
103
126
  JOIN users u ON t.user_id = u.id
127
+ LEFT JOIN user_roles ur ON ur.user_id = u.id
128
+ LEFT JOIN roles r ON ur.role_id = r.id
104
129
  WHERE t.id = %s
105
130
  """, (parsed['id'],))
106
- result = cur.fetchone()
107
- if not result:
131
+ results = cur.fetchall()
132
+ if not results:
108
133
  raise AuthError('Invalid API token')
109
134
 
135
+ # Get the first row for token/user data (all rows will have same token/user data)
136
+ result = results[0]
137
+
110
138
  # Verify the nonce
111
139
  if not bcrypt.checkpw(parsed['nonce'].encode('utf-8'), result['token'].encode('utf-8')):
112
140
  raise AuthError('Invalid API token')
@@ -115,29 +143,28 @@ class AuthManager:
115
143
  if result['expires_at'] and result['expires_at'] < datetime.utcnow():
116
144
  raise AuthError('API token has expired')
117
145
 
118
- # Update last used timestamp
119
- cur.execute("""
120
- UPDATE api_tokens
121
- SET last_used_at = %s
122
- WHERE id = %s
123
- """, (datetime.utcnow(), parsed['id']))
146
+ # Schedule last used timestamp update (asynchronous with 10s delay)
147
+ self._schedule_last_used_update(parsed['id'])
124
148
 
125
- # Fetch roles
126
- cur.execute("""
127
- SELECT r.name FROM roles r
128
- JOIN user_roles ur ON ur.role_id = r.id
129
- WHERE ur.user_id = %s
130
- """, (result['user_id'],))
131
- roles = [row['name'] for row in cur.fetchall()]
149
+ # Extract roles from results
150
+ roles = [row['role_name'] for row in results if row['role_name'] is not None]
132
151
 
133
152
  # Construct user object
134
- return {
153
+ user_data = {
135
154
  'id': result['user_id'],
136
155
  'username': result['username'],
137
156
  'email': result['email'],
138
157
  'real_name': result['real_name'],
139
158
  'roles': roles
140
159
  }
160
+
161
+ # Cache the result
162
+ self._user_cache[cache_key] = (user_data.copy(), current_time)
163
+
164
+ # Clean up expired cache entries
165
+ self._cleanup_cache()
166
+
167
+ return user_data
141
168
  except ValueError:
142
169
  raise AuthError('Invalid token format')
143
170
 
@@ -441,21 +468,42 @@ class AuthManager:
441
468
  logger.debug(f"Token payload: {payload}")
442
469
  user_id = int(payload['sub']) # Convert string ID back to integer
443
470
 
471
+ # Check cache first
472
+ cache_key = f"user_{user_id}"
473
+ current_time = datetime.utcnow()
474
+
475
+ if cache_key in self._user_cache:
476
+ cached_data, cache_time = self._user_cache[cache_key]
477
+ if (current_time - cache_time).total_seconds() < self._cache_ttl:
478
+ logger.debug(f"Returning cached user data for ID: {user_id}")
479
+ return cached_data.copy() # Return a copy to avoid modifying cache
480
+
481
+ # Cache miss or expired, fetch from database
444
482
  with self.db.get_cursor() as cur:
445
- cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
446
- user = cur.fetchone()
447
- if not user:
448
- logger.error(f"User not found for ID: {user_id}")
449
- raise AuthError('User not found', 404)
450
- # Fetch roles
451
483
  cur.execute("""
452
- SELECT r.name FROM roles r
453
- JOIN user_roles ur ON ur.role_id = r.id
454
- WHERE ur.user_id = %s
484
+ SELECT u.*, r.name as role_name FROM users u
485
+ LEFT JOIN user_roles ur ON ur.user_id = u.id
486
+ LEFT JOIN roles r ON ur.role_id = r.id
487
+ WHERE u.id = %s
455
488
  """, (user_id,))
456
- roles = [row['name'] for row in cur.fetchall()]
489
+ results = cur.fetchall()
490
+ if not results:
491
+ logger.error(f"User not found for ID: {user_id}")
492
+ raise AuthError('User not found', 404)
493
+
494
+ # Get the first row for user data (all rows will have same user data)
495
+ user = results[0]
496
+
497
+ # Extract roles from results
498
+ roles = [row['role_name'] for row in results if row['role_name'] is not None]
457
499
  user['roles'] = roles
458
500
 
501
+ # Cache the result
502
+ self._user_cache[cache_key] = (user.copy(), current_time)
503
+
504
+ # Clean up expired cache entries
505
+ self._cleanup_cache()
506
+
459
507
  return user
460
508
  except jwt.InvalidTokenError as e:
461
509
  logger.error(f"Invalid token error: {str(e)}")
@@ -464,6 +512,78 @@ class AuthManager:
464
512
  logger.error(f"Unexpected error during token validation: {str(e)}")
465
513
  raise AuthError(str(e), 500)
466
514
 
515
+ def _cleanup_cache(self):
516
+ """Remove expired cache entries."""
517
+ current_time = datetime.utcnow()
518
+ expired_keys = [
519
+ key for key, (_, cache_time) in self._user_cache.items()
520
+ if (current_time - cache_time).total_seconds() >= self._cache_ttl
521
+ ]
522
+ for key in expired_keys:
523
+ del self._user_cache[key]
524
+
525
+ def _start_update_thread(self):
526
+ """Start the background thread for processing last_used_at updates."""
527
+ if self._update_thread is None or not self._update_thread.is_alive():
528
+ self._update_thread = threading.Thread(target=self._update_worker, daemon=True)
529
+ self._update_thread.start()
530
+ logger.debug("Started background update thread")
531
+
532
+ def _schedule_last_used_update(self, token_id):
533
+ """Schedule a last_used_at update for an API token with 10s delay."""
534
+ with self._update_lock:
535
+ self._last_used_updates[token_id] = time.time()
536
+ logger.debug(f"Scheduled last_used update for token {token_id}")
537
+
538
+ def _update_worker(self):
539
+ """Background worker that processes last_used_at updates."""
540
+ while not self._shutdown_event.is_set():
541
+ try:
542
+ current_time = time.time()
543
+ tokens_to_update = []
544
+
545
+ # Collect tokens that need updating (older than 10 seconds)
546
+ with self._update_lock:
547
+ for token_id, schedule_time in list(self._last_used_updates.items()):
548
+ if current_time - schedule_time >= 10: # 10 second delay
549
+ tokens_to_update.append(token_id)
550
+ del self._last_used_updates[token_id]
551
+
552
+ # Perform batch update
553
+ if tokens_to_update:
554
+ self._perform_batch_update(tokens_to_update)
555
+
556
+ # Sleep for a short interval
557
+ time.sleep(10)
558
+
559
+ except Exception as e:
560
+ logger.error(f"Error in update worker: {e}")
561
+ time.sleep(5) # Wait longer on error
562
+
563
+ def _perform_batch_update(self, token_ids):
564
+ """Perform batch update of last_used_at for multiple tokens."""
565
+ try:
566
+ with self.db.get_cursor() as cur:
567
+ # Update all tokens in a single query
568
+ placeholders = ','.join(['%s'] * len(token_ids))
569
+ cur.execute(f"""
570
+ UPDATE api_tokens
571
+ SET last_used_at = %s
572
+ WHERE id IN ({placeholders})
573
+ """, [datetime.utcnow()] + token_ids)
574
+
575
+ logger.debug(f"Updated last_used_at for {len(token_ids)} tokens: {token_ids}")
576
+
577
+ except Exception as e:
578
+ logger.error(f"Error performing batch update: {e}")
579
+
580
+ def shutdown(self):
581
+ """Shutdown the background update thread."""
582
+ self._shutdown_event.set()
583
+ if self._update_thread and self._update_thread.is_alive():
584
+ self._update_thread.join(timeout=5)
585
+ logger.debug("Background update thread shutdown complete")
586
+
467
587
  def get_current_user(self):
468
588
  return self._authenticate_request()
469
589
 
the37lab_authlib/db.py CHANGED
@@ -1,15 +1,28 @@
1
1
  import psycopg2
2
2
  from psycopg2.extras import RealDictCursor
3
+ from psycopg2 import pool
3
4
  from contextlib import contextmanager
4
5
  from .models import UUIDGenerator, IntegerGenerator
5
6
 
6
7
  class Database:
7
- def __init__(self, dsn, id_type='uuid'):
8
+ def __init__(self, dsn, id_type='uuid', min_conn=1, max_conn=10):
8
9
  self.dsn = dsn
9
10
  self.id_generator = UUIDGenerator() if id_type == 'uuid' else IntegerGenerator()
10
11
  self.id_type = id_type
12
+ self.min_conn = min_conn
13
+ self.max_conn = max_conn
14
+ self._pool = None
15
+ self._init_pool()
11
16
  self._init_db()
12
17
 
18
+ def _init_pool(self):
19
+ self._pool = pool.ThreadedConnectionPool(
20
+ self.min_conn,
21
+ self.max_conn,
22
+ self.dsn,
23
+ cursor_factory=RealDictCursor
24
+ )
25
+
13
26
  def _init_db(self):
14
27
  with self.get_connection() as conn:
15
28
  with conn.cursor() as cur:
@@ -54,7 +67,7 @@ class Database:
54
67
 
55
68
  @contextmanager
56
69
  def get_connection(self):
57
- conn = psycopg2.connect(self.dsn, cursor_factory=RealDictCursor)
70
+ conn = self._pool.getconn()
58
71
  try:
59
72
  yield conn
60
73
  conn.commit()
@@ -62,7 +75,7 @@ class Database:
62
75
  conn.rollback()
63
76
  raise
64
77
  finally:
65
- conn.close()
78
+ self._pool.putconn(conn)
66
79
 
67
80
  @contextmanager
68
81
  def get_cursor(self):
@@ -71,4 +84,8 @@ class Database:
71
84
  yield cur
72
85
 
73
86
  def get_id_generator(self):
74
- return self.id_generator
87
+ return self.id_generator
88
+
89
+ def close(self):
90
+ if self._pool:
91
+ self._pool.closeall()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: the37lab_authlib
3
- Version: 0.1.1755164205
3
+ Version: 0.1.1756371198
4
4
  Summary: Python SDK for the Authlib
5
5
  Author-email: the37lab <info@the37lab.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -0,0 +1,10 @@
1
+ the37lab_authlib/__init__.py,sha256=cFVTWL-0YIMqwOMVy1P8mOt_bQODJp-L9bfp2QQ8CTo,132
2
+ the37lab_authlib/auth.py,sha256=x5PlHnjIw4OSIn7dOYdPw-_Ef4306Y0rJeOaVN8efXY,28985
3
+ the37lab_authlib/db.py,sha256=cmnmykKvq6V5e-D0HGiRN4DjFBOGB-SL1HpFjR5uyCw,3162
4
+ the37lab_authlib/decorators.py,sha256=L-gJUUwDUT2JXTptQ6XEey1LkI5RprbqzEfArWI7F8Y,1305
5
+ the37lab_authlib/exceptions.py,sha256=mdplK5sKNtagPAzSGq5NGsrQ4r-k03DKJBKx6myWwZc,317
6
+ the37lab_authlib/models.py,sha256=-PlvQlHGIsSdrH0H9Cdh_vTPlltGV8G1Z1mmGQvAg9Y,3422
7
+ the37lab_authlib-0.1.1756371198.dist-info/METADATA,sha256=ODFfvXVF6rycV7_gsST1K8s5_sASh5K93Qdmrn5ZRVY,8319
8
+ the37lab_authlib-0.1.1756371198.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ the37lab_authlib-0.1.1756371198.dist-info/top_level.txt,sha256=6Jmxw4UeLrhfJXgRKbXWY4OhxRSaMs0dKKhNCGWWSwc,17
10
+ the37lab_authlib-0.1.1756371198.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- the37lab_authlib/__init__.py,sha256=cFVTWL-0YIMqwOMVy1P8mOt_bQODJp-L9bfp2QQ8CTo,132
2
- the37lab_authlib/auth.py,sha256=eAeyS9wBO7euMkbnRZH8mvaUzy06KbQ3qwckOtmjhkw,23536
3
- the37lab_authlib/db.py,sha256=fTXxnfju0lmbFGPVbXpTMeDmJMeBgURVZTndyxyRyCc,2734
4
- the37lab_authlib/decorators.py,sha256=L-gJUUwDUT2JXTptQ6XEey1LkI5RprbqzEfArWI7F8Y,1305
5
- the37lab_authlib/exceptions.py,sha256=mdplK5sKNtagPAzSGq5NGsrQ4r-k03DKJBKx6myWwZc,317
6
- the37lab_authlib/models.py,sha256=-PlvQlHGIsSdrH0H9Cdh_vTPlltGV8G1Z1mmGQvAg9Y,3422
7
- the37lab_authlib-0.1.1755164205.dist-info/METADATA,sha256=pne_IT3gqIEhM6AAf8vF6bBUNOecqSUl_2RsgwQuggA,8319
8
- the37lab_authlib-0.1.1755164205.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- the37lab_authlib-0.1.1755164205.dist-info/top_level.txt,sha256=6Jmxw4UeLrhfJXgRKbXWY4OhxRSaMs0dKKhNCGWWSwc,17
10
- the37lab_authlib-0.1.1755164205.dist-info/RECORD,,