reyserver 1.1.93__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.
reyserver/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ @Time : 2023-02-19
6
+ @Author : Rey
7
+ @Contact : reyxbo@163.com
8
+ @Explain : Backend server method set.
9
+
10
+ Modules
11
+ -------
12
+ rall : All methods.
13
+ rauth : Authentication methods.
14
+ rbase : Base methods.
15
+ rbind : Dependency bind methods.
16
+ rcache : Cache methods.
17
+ rclient : Client methods.
18
+ rfile : File methods.
19
+ rpublic : Public methods.
20
+ rredirect : Redirect methods.
21
+ rserver : Server methods.
22
+ rtest : Test methods.
23
+ """
24
+
25
+
26
+ from .rserver import Server
reyserver/rall.py ADDED
@@ -0,0 +1,21 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ @Time : 2024-01-11
6
+ @Author : Rey
7
+ @Contact : reyxbo@163.com
8
+ @Explain : All methods.
9
+ """
10
+
11
+
12
+ from .rauth import *
13
+ from .rbase import *
14
+ from .rbind import *
15
+ from .rcache import *
16
+ from .rclient import *
17
+ from .rfile import *
18
+ from .rpublic import *
19
+ from .rredirect import *
20
+ from .rserver import *
21
+ from .rtest import *
reyserver/rauth.py ADDED
@@ -0,0 +1,471 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ @Time : 2025-10-10
6
+ @Author : Rey
7
+ @Contact : reyxbo@163.com
8
+ @Explain : Authentication methods.
9
+ """
10
+
11
+
12
+ from typing import Any, TypedDict, NotRequired, Literal
13
+ from datetime import datetime as Datetime
14
+ from fastapi import APIRouter, Request
15
+ from fastapi.security import OAuth2PasswordBearer
16
+ from reydb import rorm, DatabaseEngine, DatabaseEngineAsync
17
+ from reykit.rdata import encode_jwt, decode_jwt, is_hash_bcrypt
18
+ from reykit.rre import search_batch
19
+ from reykit.rtime import now
20
+
21
+ from .rbase import exit_api
22
+ from .rbind import Bind
23
+
24
+
25
+ __all__ = (
26
+ 'DatabaseORMTableUser',
27
+ 'DatabaseORMTableRole',
28
+ 'DatabaseORMTablePerm',
29
+ 'DatabaseORMTableUserRole',
30
+ 'DatabaseORMTableRolePerm',
31
+ 'build_db_auth',
32
+ 'router_auth'
33
+ )
34
+
35
+
36
+ UserData = TypedDict(
37
+ 'UserData',
38
+ {
39
+ 'create_time': float,
40
+ 'udpate_time': float,
41
+ 'user_id': int,
42
+ 'user_name': str,
43
+ 'role_names': list[str],
44
+ 'perm_names': list[str],
45
+ 'perm_apis': list[str],
46
+ 'email': str | None,
47
+ 'phone': str | None,
48
+ 'avatar': int | None,
49
+ 'password': NotRequired[str]
50
+ }
51
+ )
52
+ TokenData = TypedDict(
53
+ 'TokenData',
54
+ {
55
+ 'sub': int,
56
+ 'iat': int,
57
+ 'nbf': int,
58
+ 'exp': int,
59
+ 'user': UserData
60
+ }
61
+ )
62
+ Token = str
63
+ JSONToken = TypedDict(
64
+ 'JSONToken',
65
+ {
66
+ 'access_token': Token,
67
+ 'token_type': Literal['Bearer']
68
+ }
69
+ )
70
+
71
+
72
+ class DatabaseORMTableUser(rorm.Table):
73
+ """
74
+ Database "user" table ORM model.
75
+ """
76
+
77
+ __name__ = 'user'
78
+ __comment__ = 'User information table.'
79
+ create_time: rorm.Datetime = rorm.Field(field_default=':time', not_null=True, index_n=True, comment='Record create time.')
80
+ update_time: rorm.Datetime = rorm.Field(field_default=':time', not_null=True, index_n=True, comment='Record update time.')
81
+ user_id: int = rorm.Field(key_auto=True, comment='User ID.')
82
+ name: str = rorm.Field(rorm.types.VARCHAR(50), not_null=True, index_u=True, comment='User name, use lowercase letters.')
83
+ password: str = rorm.Field(rorm.types.CHAR(60), not_null=True, comment='User password, encrypted with "bcrypt".')
84
+ email: rorm.Email = rorm.Field(rorm.types.VARCHAR(255), index_u=True, comment='User email.')
85
+ phone: str = rorm.Field(rorm.types.CHAR(11), index_u=True, comment='User phone.')
86
+ avatar: int = rorm.Field(comment='User avatar file ID.')
87
+ is_valid: bool = rorm.Field(field_default='TRUE', not_null=True, comment='Is the valid.')
88
+
89
+
90
+ class DatabaseORMTableRole(rorm.Table):
91
+ """
92
+ Database "role" table ORM model.
93
+ """
94
+
95
+ __name__ = 'role'
96
+ __comment__ = 'Role information table.'
97
+ create_time: rorm.Datetime = rorm.Field(field_default=':time', not_null=True, index_n=True, comment='Record create time.')
98
+ update_time: rorm.Datetime = rorm.Field(field_default=':time', arg_default=now, not_null=True, index_n=True, comment='Record update time.')
99
+ role_id: int = rorm.Field(rorm.types.SMALLINT, key_auto=True, comment='Role ID.')
100
+ name: str = rorm.Field(rorm.types.VARCHAR(50), not_null=True, index_u=True, comment='Role name.')
101
+ desc: str = rorm.Field(rorm.types.VARCHAR(500), comment='Role description.')
102
+ is_valid: bool = rorm.Field(field_default='TRUE', not_null=True, comment='Is the valid.')
103
+
104
+
105
+ class DatabaseORMTablePerm(rorm.Table):
106
+ """
107
+ Database "perm" table ORM model.
108
+ """
109
+
110
+ __name__ = 'perm'
111
+ __comment__ = 'API permission information table.'
112
+ create_time: rorm.Datetime = rorm.Field(field_default=':time', not_null=True, index_n=True, comment='Record create time.')
113
+ update_time: rorm.Datetime = rorm.Field(field_default=':time', arg_default=now, not_null=True, index_n=True, comment='Record update time.')
114
+ perm_id: int = rorm.Field(rorm.types.SMALLINT, key_auto=True, comment='Permission ID.')
115
+ name: str = rorm.Field(rorm.types.VARCHAR(50), not_null=True, index_u=True, comment='Permission name.')
116
+ desc: str = rorm.Field(rorm.types.VARCHAR(500), comment='Permission description.')
117
+ api: str = rorm.Field(
118
+ rorm.types.VARCHAR(1000),
119
+ comment=r'API method and resource path regular expression "match" pattern, case insensitive, format is "{method} {path}" (e.g. "GET /users").'
120
+ )
121
+ is_valid: bool = rorm.Field(field_default='TRUE', not_null=True, comment='Is the valid.')
122
+
123
+
124
+ class DatabaseORMTableUserRole(rorm.Table):
125
+ """
126
+ Database "user_role" table ORM model.
127
+ """
128
+
129
+ __name__ = 'user_role'
130
+ __comment__ = 'User and role association table.'
131
+ create_time: rorm.Datetime = rorm.Field(field_default=':time', not_null=True, index_n=True, comment='Record create time.')
132
+ update_time: rorm.Datetime = rorm.Field(field_default=':time', arg_default=now, not_null=True, index_n=True, comment='Record update time.')
133
+ user_id: int = rorm.Field(key=True, comment='User ID.')
134
+ role_id: int = rorm.Field(rorm.types.SMALLINT, key=True, comment='Role ID.')
135
+
136
+
137
+ class DatabaseORMTableRolePerm(rorm.Table):
138
+ """
139
+ Database "role_perm" table ORM model.
140
+ """
141
+
142
+ __name__ = 'role_perm'
143
+ __comment__ = 'role and permission association table.'
144
+ create_time: rorm.Datetime = rorm.Field(field_default=':time', not_null=True, index_n=True, comment='Record create time.')
145
+ update_time: rorm.Datetime = rorm.Field(field_default=':time', arg_default=now, not_null=True, index_n=True, comment='Record update time.')
146
+ role_id: int = rorm.Field(rorm.types.SMALLINT, key=True, comment='Role ID.')
147
+ perm_id: int = rorm.Field(rorm.types.SMALLINT, key=True, comment='Permission ID.')
148
+
149
+
150
+ def build_db_auth(engine: DatabaseEngine | DatabaseEngineAsync) -> None:
151
+ """
152
+ Check and build "auth" database tables.
153
+
154
+ Parameters
155
+ ----------
156
+ db : Database engine instance.
157
+ """
158
+
159
+ # Set parameter.
160
+
161
+ ## Table.
162
+ tables = [
163
+ DatabaseORMTableUser,
164
+ DatabaseORMTableRole,
165
+ DatabaseORMTablePerm,
166
+ DatabaseORMTableUserRole,
167
+ DatabaseORMTableRolePerm
168
+ ]
169
+
170
+ ## View stats.
171
+ views_stats = [
172
+ {
173
+ 'table': 'stats',
174
+ 'items': [
175
+ {
176
+ 'name': 'user_count',
177
+ 'select': (
178
+ 'SELECT COUNT(1)\n'
179
+ 'FROM "user"'
180
+ ),
181
+ 'comment': 'User information count.'
182
+ },
183
+ {
184
+ 'name': 'role_count',
185
+ 'select': (
186
+ 'SELECT COUNT(1)\n'
187
+ 'FROM "role"'
188
+ ),
189
+ 'comment': 'Role information count.'
190
+ },
191
+ {
192
+ 'name': 'perm_count',
193
+ 'select': (
194
+ 'SELECT COUNT(1)\n'
195
+ 'FROM "perm"'
196
+ ),
197
+ 'comment': 'Permission information count.'
198
+ },
199
+ {
200
+ 'name': 'user_day_count',
201
+ 'select': (
202
+ 'SELECT COUNT(1)\n'
203
+ 'FROM "user"\n'
204
+ 'WHERE DATE_PART(\'day\', NOW() - "create_time") = 0'
205
+ ),
206
+ 'comment': 'User information count in the past day.'
207
+ },
208
+ {
209
+ 'name': 'user_week_count',
210
+ 'select': (
211
+ 'SELECT COUNT(1)\n'
212
+ 'FROM "user"\n'
213
+ 'WHERE DATE_PART(\'day\', NOW() - "create_time") <= 6'
214
+ ),
215
+ 'comment': 'User information count in the past week.'
216
+ },
217
+ {
218
+ 'name': 'user_month_count',
219
+ 'select': (
220
+ 'SELECT COUNT(1)\n'
221
+ 'FROM "user"\n'
222
+ 'WHERE DATE_PART(\'day\', NOW() - "create_time") <= 29'
223
+ ),
224
+ 'comment': 'User information count in the past month.'
225
+ },
226
+ {
227
+ 'name': 'user_last_time',
228
+ 'select': (
229
+ 'SELECT MAX("create_time")\n'
230
+ 'FROM "user"'
231
+ ),
232
+ 'comment': 'User last record create time.'
233
+ }
234
+ ]
235
+ }
236
+ ]
237
+
238
+ # Build.
239
+ engine.sync_engine.build.build(tables=tables, views_stats=views_stats, skip=True)
240
+
241
+
242
+ bearer = OAuth2PasswordBearer(
243
+ tokenUrl='/token',
244
+ scheme_name='OAuth2Password',
245
+ description='Authentication of OAuth2 password model.',
246
+ auto_error=False
247
+ )
248
+
249
+
250
+ async def depend_token(
251
+ request: Request,
252
+ server: Bind.Server = Bind.server,
253
+ token: Token | None = Bind.Depend(bearer)
254
+ ) -> TokenData:
255
+ """
256
+ Dependencie function of authentication token.
257
+ If the verification fails, then response status code is 401 or 403.
258
+
259
+ Parameters
260
+ ----------
261
+ request : Request.
262
+ server : Server.
263
+ token : Authentication token.
264
+
265
+ Returns
266
+ -------
267
+ Token data.
268
+ """
269
+
270
+ # Check.
271
+ if not server.is_started_auth:
272
+ return
273
+ if bearer is None:
274
+ exit_api(401)
275
+
276
+ # Parameter.
277
+ key = server.api_auth_key
278
+ api_path = f'{request.method} {request.url.path}'
279
+
280
+ # Cache.
281
+ token_data: UserData | None = getattr(request.state, 'token_data', None)
282
+
283
+ # Decode.
284
+ if token_data is None:
285
+ token_data: TokenData | None = decode_jwt(token, key)
286
+ if token_data is None:
287
+ exit_api(401)
288
+ request.state.token_data = token_data
289
+
290
+ # Authentication.
291
+ perm_apis = token_data['user']['perm_apis']
292
+ perm_apis = [
293
+ f'^{pattern}$'
294
+ for pattern in perm_apis
295
+ ]
296
+ result = search_batch(api_path, *perm_apis)
297
+ if result is None:
298
+ exit_api(403)
299
+
300
+ return token_data
301
+
302
+
303
+ Bind.token = Bind.Depend(depend_token)
304
+
305
+ router_auth = APIRouter()
306
+
307
+
308
+ async def get_user_data(
309
+ conn: Bind.Conn,
310
+ account: str,
311
+ account_type: Literal['user_id', 'name', 'email', 'phone'] = 'name',
312
+ filter_invalid: bool = True
313
+ ) -> UserData | None:
314
+ """
315
+ Get user data.
316
+
317
+ Parameters
318
+ ----------
319
+ conn: Asyncronous database connection.
320
+ account : User account.
321
+ account_type : User account type.
322
+ - "Literal['name']": User name.
323
+ - "Literal['email']": User Email.
324
+ - "Literal['phone']": User phone mumber.
325
+ filter_invalid : Whether filter invalid user.
326
+
327
+ Returns
328
+ -------
329
+ User data or null.
330
+ """
331
+
332
+ # Parameters.
333
+ if filter_invalid:
334
+ sql_where_user = (
335
+ ' WHERE (\n'
336
+ f' "{account_type}" = :account\n'
337
+ ' AND "is_valid" = TRUE\n'
338
+ ' )\n'
339
+ )
340
+ sql_where_role = sql_where_perm = ' WHERE "is_valid" = TRUE\n'
341
+ else:
342
+ sql_where_user = ' WHERE "{account_type}" = :account\n'
343
+ sql_where_role = sql_where_perm = ''
344
+
345
+ # Get.
346
+ sql = (
347
+ 'SELECT ANY_VALUE("create_time") AS "create_time",\n'
348
+ ' ANY_VALUE("phone") AS "phone",\n'
349
+ ' ANY_VALUE("update_time") AS "update_time",\n'
350
+ ' ANY_VALUE("user"."user_id") AS "user_id",\n'
351
+ ' ANY_VALUE("user"."name") AS "user_name",\n'
352
+ ' ANY_VALUE("password") AS "password",\n'
353
+ ' ANY_VALUE("email") AS "email",\n'
354
+ ' ANY_VALUE("avatar") AS "avatar",\n'
355
+ ' STRING_AGG(DISTINCT "role"."name", \';\') AS "role_names",\n'
356
+ ' STRING_AGG(DISTINCT "perm"."name", \';\') AS "perm_names",\n'
357
+ ' STRING_AGG(DISTINCT "perm"."api", \';\') AS "perm_apis"\n'
358
+ 'FROM (\n'
359
+ ' SELECT "create_time", "update_time", "user_id", "password", "name", "email", "phone", "avatar"\n'
360
+ ' FROM "user"\n'
361
+ f'{sql_where_user}'
362
+ ' LIMIT 1\n'
363
+ ') as "user"\n'
364
+ 'LEFT JOIN (\n'
365
+ ' SELECT "user_id", "role_id"\n'
366
+ ' FROM "user_role"\n'
367
+ ') as "user_role"\n'
368
+ 'ON "user_role"."user_id" = "user"."user_id"\n'
369
+ 'LEFT JOIN (\n'
370
+ ' SELECT "role_id", "name"\n'
371
+ ' FROM "role"\n'
372
+ f'{sql_where_role}'
373
+ ') AS "role"\n'
374
+ 'ON "user_role"."role_id" = "role"."role_id"\n'
375
+ 'LEFT JOIN (\n'
376
+ ' SELECT "role_id", "perm_id"\n'
377
+ ' FROM "role_perm"\n'
378
+ ') as "role_perm"\n'
379
+ 'ON "role_perm"."role_id" = "role"."role_id"\n'
380
+ 'LEFT JOIN (\n'
381
+ ' SELECT "perm_id", "name", "api"\n'
382
+ ' FROM "perm"\n'
383
+ f'{sql_where_perm}'
384
+ ') AS "perm"\n'
385
+ 'ON "role_perm"."perm_id" = "perm"."perm_id"\n'
386
+ 'GROUP BY "user"."user_id"'
387
+ )
388
+ result = await conn.execute(
389
+ sql,
390
+ account=account
391
+ )
392
+
393
+ # Extract.
394
+ if result.empty:
395
+ info = None
396
+ else:
397
+ row: dict[str, Datetime | Any] = result.to_row()
398
+ if row['role_names'] is None:
399
+ row['role_names'] = ''
400
+ if row['perm_names'] is None:
401
+ row['perm_names'] = ''
402
+ if row['perm_apis'] is None:
403
+ row['perm_apis'] = ''
404
+ info: UserData = {
405
+ 'create_time': row['create_time'].timestamp(),
406
+ 'udpate_time': row['update_time'].timestamp(),
407
+ 'user_id': row['user_id'],
408
+ 'user_name': row['user_name'],
409
+ 'role_names': row['role_names'].split(';'),
410
+ 'perm_names': row['perm_names'].split(';'),
411
+ 'perm_apis': row['perm_apis'].split(';'),
412
+ 'email': row['email'],
413
+ 'phone': row['phone'],
414
+ 'avatar': row['avatar'],
415
+ 'password': row['password']
416
+ }
417
+
418
+ return info
419
+
420
+
421
+ @router_auth.post('/token')
422
+ async def create_token(
423
+ username: str = Bind.i.form,
424
+ password: str = Bind.i.form,
425
+ conn: Bind.Conn = Bind.conn.auth,
426
+ server: Bind.Server = Bind.server
427
+ ) -> JSONToken:
428
+ """
429
+ Create token.
430
+
431
+ Parameters
432
+ ----------
433
+ username : User name.
434
+ password : User password.
435
+
436
+ Returns
437
+ -------
438
+ JSON with "token".
439
+ """
440
+
441
+ # Parameter.
442
+ key = server.api_auth_key
443
+ sess_seconds = server.api_auth_sess_seconds
444
+
445
+ # User data.
446
+ user_data = await get_user_data(conn, username)
447
+
448
+ # Check.
449
+ if user_data is None:
450
+ exit_api(401)
451
+ password_hash = user_data.pop('password')
452
+ if not is_hash_bcrypt(password, password_hash):
453
+ exit_api(401)
454
+
455
+ # Response.
456
+ now_timestamp_s = now('timestamp_s')
457
+ user_id = user_data.pop('user_id')
458
+ data: TokenData = {
459
+ 'sub': str(user_id),
460
+ 'iat': now_timestamp_s,
461
+ 'nbf': now_timestamp_s,
462
+ 'exp': now_timestamp_s + sess_seconds,
463
+ 'user': user_data
464
+ }
465
+ token = encode_jwt(data, key)
466
+ response = {
467
+ 'access_token': token,
468
+ 'token_type': 'Bearer'
469
+ }
470
+
471
+ return response
reyserver/rbase.py ADDED
@@ -0,0 +1,74 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ @Time : 2025-07-17
6
+ @Author : Rey
7
+ @Contact : reyxbo@163.com
8
+ @Explain : Base methods.
9
+ """
10
+
11
+
12
+ from typing import NoReturn
13
+ from http import HTTPStatus
14
+ from fastapi import HTTPException
15
+ from fastapi.params import Depends
16
+ from reykit.rbase import Base, Exit, throw
17
+
18
+
19
+ __all__ = (
20
+ 'ServerBase',
21
+ 'ServerExit',
22
+ 'ServerExitAPI',
23
+ 'exit_api',
24
+ 'depend_pass'
25
+ )
26
+
27
+
28
+ class ServerBase(Base):
29
+ """
30
+ Server base type.
31
+ """
32
+
33
+
34
+ class ServerExit(ServerBase, Exit):
35
+ """
36
+ Server exit type.
37
+ """
38
+
39
+
40
+ class ServerExitAPI(ServerExit, HTTPException):
41
+ """
42
+ Server exit API type.
43
+ """
44
+
45
+
46
+ def exit_api(code: int = 400, text: str | None = None) -> NoReturn:
47
+ """
48
+ Throw exception to exit API.
49
+
50
+ Parameters
51
+ ----------
52
+ code : Response status code.
53
+ text : Explain text.
54
+ `None`: Use Default text.
55
+ """
56
+
57
+ # Parameter.
58
+ if not 400 <= code <= 499:
59
+ throw(ValueError, code)
60
+ if text is None:
61
+ status = HTTPStatus(code)
62
+ text = status.description
63
+
64
+ # Throw exception.
65
+ raise ServerExitAPI(code, text)
66
+
67
+
68
+ async def depend_pass_func() -> None:
69
+ """
70
+ Depend pass.
71
+ """
72
+
73
+
74
+ depend_pass = Depends(depend_pass_func)