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/rserver.py ADDED
@@ -0,0 +1,432 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ @Time : 2025-10-05
6
+ @Author : Rey
7
+ @Contact : reyxbo@163.com
8
+ @Explain : Server methods.
9
+ """
10
+
11
+
12
+ from typing import Literal
13
+ from collections.abc import Sequence, Callable, Coroutine
14
+ from inspect import iscoroutinefunction
15
+ from contextlib import asynccontextmanager, _AsyncGeneratorContextManager
16
+ from uvicorn import run as uvicorn_run
17
+ from starlette.middleware.base import _StreamingResponse
18
+ from fastapi import FastAPI, Request
19
+ from fastapi.staticfiles import StaticFiles
20
+ from fastapi.middleware.gzip import GZipMiddleware
21
+ from fastapi.middleware.trustedhost import TrustedHostMiddleware
22
+ from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
23
+ from fastapi_cache import FastAPICache
24
+ from redis.asyncio import Redis
25
+ from reydb import DatabaseAsync
26
+ from reykit.rbase import CoroutineFunctionSimple, Singleton, throw
27
+ from reykit.ros import FileStore
28
+ from reykit.rrand import randchar
29
+
30
+ from .rbase import ServerBase
31
+ from .rbind import Bind
32
+ from .rcache import init_cache
33
+
34
+
35
+ __all__ = (
36
+ 'Server',
37
+ )
38
+
39
+
40
+ class Server(ServerBase, Singleton):
41
+ """
42
+ Server type, singleton mode.
43
+ Based on `fastapi` and `uvicorn` package.
44
+ Can view document api '/docs', '/redoc', '/openapi.json'.
45
+ """
46
+
47
+ is_started_auth: bool = False
48
+ 'Whether start authentication.'
49
+ api_public_dir: str
50
+ 'Public directory.'
51
+ api_redirect_server_url: str
52
+ 'Target server URL of redirect all requests.'
53
+ api_auth_key: str
54
+ 'Authentication API JWT encryption key.'
55
+ api_auth_sess_seconds: int
56
+ 'Authentication API session valid seconds.'
57
+ api_file_store: FileStore
58
+ 'File API store instance.'
59
+
60
+
61
+ def __init__(
62
+ self,
63
+ db: DatabaseAsync | None = None,
64
+ db_warm: bool = False,
65
+ redis: Redis | None = None,
66
+ redis_expire: int | None = None,
67
+ depend: CoroutineFunctionSimple | Sequence[CoroutineFunctionSimple] | None = None,
68
+ before: CoroutineFunctionSimple | Sequence[CoroutineFunctionSimple] | None = None,
69
+ after: CoroutineFunctionSimple | Sequence[CoroutineFunctionSimple] | None = None,
70
+ debug: bool = False
71
+ ) -> None:
72
+ """
73
+ Build instance attributes.
74
+
75
+ Parameters
76
+ ----------
77
+ db : Asynchronous database, include database engines required for APIs.
78
+ db_warm : Whether database pre create connection to warm all pool.
79
+ redis : Asynchronous Redis, activate cache function.
80
+ redis_expire : Redis cache expire seconds.
81
+ depend : Global api dependencies.
82
+ before : Execute before server start.
83
+ after : Execute after server end.
84
+ debug : Whether use development mode debug server.
85
+ """
86
+
87
+ # Parameter.
88
+ if depend is None:
89
+ depend = ()
90
+ elif iscoroutinefunction(depend):
91
+ depend = (depend,)
92
+ depend = [
93
+ Bind.Depend(task)
94
+ for task in depend
95
+ ]
96
+ lifespan = self.__create_lifespan(
97
+ before,
98
+ after,
99
+ db_warm,
100
+ redis_expire
101
+ )
102
+
103
+ # Build.
104
+ self.db = db
105
+ self.redis = redis
106
+ self.app = FastAPI(
107
+ dependencies=depend,
108
+ lifespan=lifespan,
109
+ debug=debug,
110
+ server=self
111
+ )
112
+
113
+ # Middleware
114
+ self.wrap_middleware = self.app.middleware('http')
115
+ 'Decorator, add middleware to APP.'
116
+ self.app.add_middleware(GZipMiddleware)
117
+ self.app.add_middleware(TrustedHostMiddleware)
118
+ self.__add_base_middleware()
119
+
120
+
121
+ def __create_lifespan(
122
+ self,
123
+ before: CoroutineFunctionSimple | Sequence[CoroutineFunctionSimple] | None,
124
+ after: CoroutineFunctionSimple | Sequence[CoroutineFunctionSimple] | None,
125
+ db_warm: bool,
126
+ redis_expire: int | None
127
+ ) -> _AsyncGeneratorContextManager[None, None]:
128
+ """
129
+ Create asynchronous function of lifespan manager.
130
+
131
+ Parameters
132
+ ----------
133
+ before : Execute before server start.
134
+ after : Execute after server end.
135
+ db_warm : Whether database pre create connection to warm all pool.
136
+ redis_expire : Redis cache expire seconds.
137
+
138
+ Returns
139
+ -------
140
+ Asynchronous function.
141
+ """
142
+
143
+ # Parameter.
144
+ if before is None:
145
+ before = ()
146
+ elif iscoroutinefunction(before):
147
+ before = (before,)
148
+ if after is None:
149
+ after = ()
150
+ elif iscoroutinefunction(after):
151
+ after = (after,)
152
+
153
+
154
+ @asynccontextmanager
155
+ async def lifespan(app: FastAPI):
156
+ """
157
+ Server lifespan manager.
158
+
159
+ Parameters
160
+ ----------
161
+ app : Server APP.
162
+ """
163
+
164
+ # Before.
165
+ for task in before:
166
+ await task()
167
+
168
+ ## Databse.
169
+ if db_warm:
170
+ await self.db.warm_all()
171
+
172
+ ## Redis.
173
+ if self.redis is not None:
174
+ init_cache(self.redis, redis_expire)
175
+ else:
176
+ FastAPICache._enable = False
177
+
178
+ # Runing.
179
+ yield
180
+
181
+ # After.
182
+ for task in after:
183
+ await after()
184
+
185
+ ## Database.
186
+ if self.db is not None:
187
+ await self.db.dispose_all()
188
+
189
+
190
+ return lifespan
191
+
192
+
193
+ def __add_base_middleware(self) -> None:
194
+ """
195
+ Add base middleware.
196
+ """
197
+
198
+ # Add.
199
+ @self.wrap_middleware
200
+ async def base_middleware(
201
+ request: Request,
202
+ call_next: Callable[[Request], Coroutine[None, None, _StreamingResponse]]
203
+ ) -> _StreamingResponse:
204
+ """
205
+ Base middleware.
206
+
207
+ Parameters
208
+ ----------
209
+ Reqeust : Request instance.
210
+ call_next : Next middleware.
211
+ """
212
+
213
+ # Before.
214
+ ...
215
+
216
+ # Next.
217
+ response = await call_next(request)
218
+
219
+ # After.
220
+ if (
221
+ response.status_code == 200
222
+ and request.method == 'POST'
223
+ ):
224
+ response.status_code = 201
225
+ elif response.status_code == 401:
226
+ response.headers.setdefault('WWW-Authenticate', 'Bearer')
227
+
228
+ return response
229
+
230
+
231
+ def run(
232
+ self,
233
+ app: str | None = None,
234
+ host: str = '127.0.0.1',
235
+ port: int = 8000,
236
+ workers: int = 1,
237
+ ssl_cert: str | None = None,
238
+ ssl_key: str | None = None
239
+ ) -> None:
240
+ """
241
+ Run server.
242
+
243
+ Parameters
244
+ ----------
245
+ app : Application or function path.
246
+ - `None`: Cannot use parameter `workers`.
247
+ - `Application`: format is `module[.sub....]:var[.attr....]` (e.g. `module.sub:server.app`).
248
+ - `Function`: format is `module[.sub....]:func` (e.g. `module.sub:main`).
249
+ host : Server host.
250
+ port: Server port.
251
+ workers: Number of server work processes.
252
+ ssl_cert : SSL certificate file path.
253
+ ssl_key : SSL key file path.
254
+
255
+ Examples
256
+ --------
257
+ Single work process.
258
+ >>> server = Server(db)
259
+ >>> server.run()
260
+
261
+ Multiple work processes.
262
+ >>> server = Server(db)
263
+ >>> if __name__ == '__main__':
264
+ >>> server.run('module.sub:server.app', workers=2)
265
+ """
266
+
267
+ # Parameter.
268
+ if type(ssl_cert) != type(ssl_key):
269
+ throw(AssertionError, ssl_cert, ssl_key)
270
+ if app is None:
271
+ app = self.app
272
+ if workers == 1:
273
+ workers = None
274
+
275
+ # Run.
276
+ uvicorn_run(
277
+ app,
278
+ host=host,
279
+ port=port,
280
+ workers=workers,
281
+ ssl_certfile=ssl_cert,
282
+ ssl_keyfile=ssl_key
283
+ )
284
+
285
+
286
+ __call__ = run
287
+
288
+
289
+ def set_doc(
290
+ self,
291
+ version: str | None = None,
292
+ title: str | None = None,
293
+ summary: str | None = None,
294
+ desc: str | None = None,
295
+ contact: dict[Literal['name', 'email', 'url'], str] | None = None
296
+ ) -> None:
297
+ """
298
+ Set server document.
299
+
300
+ Parameters
301
+ ----------
302
+ version : Server version.
303
+ title : Server title.
304
+ summary : Server summary.
305
+ desc : Server description.
306
+ contact : Server contact information.
307
+ """
308
+
309
+ # Parameter.
310
+ set_dict = {
311
+ 'version': version,
312
+ 'title': title,
313
+ 'summary': summary,
314
+ 'description': desc,
315
+ 'contact': contact
316
+ }
317
+
318
+ # Set.
319
+ for key, value in set_dict.items():
320
+ if value is not None:
321
+ setattr(self.app, key, value)
322
+
323
+
324
+ def add_api_test(self) -> None:
325
+ """
326
+ Add test API.
327
+ """
328
+
329
+ from .rtest import router_test
330
+
331
+ # Add.
332
+ self.app.include_router(router_test, tags=['test'])
333
+
334
+
335
+ def add_api_public(self, public_dir: str) -> None:
336
+ """
337
+ Add public API,
338
+ mapping `{public_dir}/index.html` to `GET /`,
339
+ mapping `{public_dir}/{path}` to `GET `/public/{path:path}`.
340
+
341
+ Parameters
342
+ ----------
343
+ public_dir : Public directory.
344
+ """
345
+
346
+ from .rpublic import router_public
347
+
348
+ # Add.
349
+ self.api_public_dir = public_dir
350
+ subapp = StaticFiles(directory=public_dir)
351
+ self.app.mount('/public', subapp)
352
+ self.app.include_router(router_public, tags=['public'])
353
+
354
+
355
+ def add_api_redirect_all(self, server_url: str) -> None:
356
+ """
357
+ Add redirect all API.
358
+ Redirect all requests to the target server.
359
+
360
+ Parameters
361
+ ----------
362
+ server_url : Target server URL.
363
+ """
364
+
365
+ from .rredirect import router_redirect
366
+
367
+ # Add.
368
+ self.api_redirect_server_url = server_url
369
+ self.app.include_router(router_redirect, tags=['redirect'])
370
+
371
+
372
+ def add_api_auth(self, key: str | None = None, sess_seconds: int = 28800) -> None:
373
+ """
374
+ Add authentication API.
375
+ Note: must include database engine of `auth` name.
376
+
377
+ Parameters
378
+ ----------
379
+ key : JWT encryption key.
380
+ - `None`: Random 32 length string.
381
+ sess_seconds : Session valid seconds.
382
+ """
383
+
384
+ from .rauth import build_db_auth, router_auth
385
+
386
+ # Parameter.
387
+ if key is None:
388
+ key = randchar(32)
389
+
390
+ # Database.
391
+ if (
392
+ self.db is None
393
+ or 'auth' not in self.db
394
+ ):
395
+ throw(TypeError, self.db)
396
+ engine = self.db.auth
397
+ build_db_auth(engine)
398
+
399
+ # Add.
400
+ self.api_auth_key = key
401
+ self.api_auth_sess_seconds = sess_seconds
402
+ self.app.include_router(router_auth, tags=['auth'])
403
+ self.is_started_auth = True
404
+
405
+
406
+ def add_api_file(self, file_dir: str = 'file') -> None:
407
+ """
408
+ Add file API.
409
+ Note: must include database engine of `file` name.
410
+
411
+ Parameters
412
+ ----------
413
+ file_dir : File API store directory path.
414
+ """
415
+
416
+ from .rfile import build_db_file, router_file
417
+
418
+ # Database.
419
+ if (
420
+ self.db is None
421
+ or 'file' not in self.db
422
+ ):
423
+ throw(TypeError, self.db)
424
+ engine = self.db.file
425
+ build_db_file(engine)
426
+
427
+ # Add.
428
+ self.api_file_store = FileStore(file_dir)
429
+ self.app.include_router(router_file, tags=['file'], dependencies=(Bind.token,))
430
+
431
+
432
+ Bind.Server = Server
reyserver/rtest.py ADDED
@@ -0,0 +1,80 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ @Time : 2025-10-21
6
+ @Author : Rey
7
+ @Contact : reyxbo@163.com
8
+ @Explain : Test methods.
9
+ """
10
+
11
+
12
+ from typing import Literal
13
+ from fastapi import APIRouter
14
+ from reykit.rtask import async_sleep
15
+
16
+ from .rbind import Bind
17
+
18
+
19
+ __all__ = (
20
+ 'router_test',
21
+ )
22
+
23
+
24
+ router_test = APIRouter()
25
+
26
+
27
+ @router_test.get('/test')
28
+ def test() -> Literal['test']:
29
+ """
30
+ Test.
31
+
32
+ Returns
33
+ -------
34
+ Text `test`.
35
+ """
36
+
37
+ # Resposne.
38
+ response = 'test'
39
+
40
+ return response
41
+
42
+
43
+ @router_test.post('/test/echo')
44
+ def test_echo(data: dict = Bind.i.body) -> dict:
45
+ """
46
+ Echo test.
47
+
48
+ Paremeters
49
+ ----------
50
+ data : Echo data.
51
+
52
+ Returns
53
+ -------
54
+ Echo data.
55
+ """
56
+
57
+ return data
58
+
59
+
60
+ @router_test.get('/test/wait')
61
+ def test_wait(second: float = Bind.Query(1, gt=0, le=10)) -> Literal['test']:
62
+ """
63
+ Wait test.
64
+
65
+ Paremeters
66
+ ----------
67
+ second : Wait seconds, range is `(0-10]`.
68
+
69
+ Returns
70
+ -------
71
+ Text `test`.
72
+ """
73
+
74
+ # Sleep.
75
+ async_sleep(second)
76
+
77
+ # Resposne.
78
+ response = 'test'
79
+
80
+ return response
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: reyserver
3
+ Version: 1.1.93
4
+ Summary: Backend server method set.
5
+ Project-URL: homepage, https://github.com/reyxbo/reyserver/
6
+ Author-email: Rey <reyxbo@163.com>
7
+ License: Copyright 2025 ReyXBo
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14
+ License-File: LICENSE
15
+ Keywords: API,async,asynchronous,backend,rey,reyxbo,server
16
+ Requires-Python: >=3.12
17
+ Requires-Dist: fastapi-cache2[redis]
18
+ Requires-Dist: fastapi[standard]
19
+ Requires-Dist: reydb
20
+ Requires-Dist: reykit
21
+ Requires-Dist: sqladmin[full]
22
+ Requires-Dist: uvicorn[standard]
23
+ Description-Content-Type: text/markdown
24
+
25
+ # reyserver
26
+
27
+ > Backend server method set.
28
+
29
+ ## Install
30
+
31
+ ```
32
+ pip install reyserver
33
+ ```
@@ -0,0 +1,16 @@
1
+ reyserver/__init__.py,sha256=WnSqy03gZLHltfWe7bbg-q8-z-fn9UF9tokkXTc3ycI,513
2
+ reyserver/rall.py,sha256=ZK-Dn7wmG_GkiCefN7Fx5UJ3r8w_1gJ7OhyPP8__M0s,387
3
+ reyserver/rauth.py,sha256=o0rXDfyvdpb7LPQnEAOAgE2CbpzONMqwc9FwdSAOKsA,15319
4
+ reyserver/rbase.py,sha256=JVgFso6Aohow1sEgZCH83xB90sriZPv6DHlNwGTfOhw,1314
5
+ reyserver/rbind.py,sha256=zGXRzKWbhhFNJIdAj-cqO307fVhs3yzDBAKnAy2ixt4,6930
6
+ reyserver/rcache.py,sha256=DLNea4gcLKjh3pkuI2A9aDV0lNtw6dzWuREGzptS1wk,2756
7
+ reyserver/rclient.py,sha256=bl6Qqlz3s9DvsBuOdLMKTIN7ye_SBjASGsf5clNtTNc,6227
8
+ reyserver/rfile.py,sha256=nRW1uiKA1S6QM-VluWos3PKX9lMkIJuD31EL5kC_0Ww,9031
9
+ reyserver/rpublic.py,sha256=ixKDWdDihdVQ8Pqjpril7veJ9kkFK9yiRm8CTwmck4Q,1095
10
+ reyserver/rredirect.py,sha256=c7wgXn5UeF53irMlbJ8vctec2KEc3DuHhBOa8NBoFjA,857
11
+ reyserver/rserver.py,sha256=K6o4xzIYehT8eXW9qghBpsltqqqymktB4UvIcUpRq00,11984
12
+ reyserver/rtest.py,sha256=iNvKqueiW4rfFBtScAoRE9zC3y6mjT-rnRouJuZ5Ihk,1178
13
+ reyserver-1.1.93.dist-info/METADATA,sha256=EyyszMTDcBUgUW5sUrWEGLgkHCcnsrFqIMYI32QZw28,1703
14
+ reyserver-1.1.93.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
15
+ reyserver-1.1.93.dist-info/licenses/LICENSE,sha256=UYLPqp7BvPiH8yEZduJqmmyEl6hlM3lKrFIefiD4rvk,1059
16
+ reyserver-1.1.93.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,7 @@
1
+ Copyright 2025 ReyXBo
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.