Skip to content

Commit 323f1a1

Browse files
committed
add adapt() method for UtilMeta service, upgrade RequestContextVar factory to multiple-fallback strategy, add clear_daily option for Operations system, fix deprecated aioredis import, add details to docs
1 parent bbfc09b commit 323f1a1

26 files changed

Lines changed: 302 additions & 61 deletions

File tree

.github/workflows/test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ jobs:
9090
pip install flake8 pytest pytest-cov pytest-asyncio
9191
pip install jwcrypto psutil jwt
9292
pip install utype
93-
pip install databases[aiosqlite] redis aioredis psycopg2 mysqlclient
93+
pip install databases[aiosqlite] redis>=4.2.0rc1 psycopg2 mysqlclient
9494
pip install django==${{ matrix.django-version }}
9595
pip install flask apiflask fastapi tornado aiohttp uvicorn httpx requests python-multipart
9696
pip install sanic==24.6.0

docs/en/guide/config-run.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Run & Deploy
1+
# Configure, Run & Deploy
22

33
This document describes how to configure and run the UtilMeta service and how to deploy it in a production environment.
44

@@ -24,6 +24,7 @@ The first parameter of the UtilMeta service receives the name of the current mod
2424
* `asynchronous`: whether the service provides an asynchronous APIs. The default is determined by the runtime framework.
2525
* `api`: pass in UtilMeta’s root API class or its reference string
2626
* `route`: pass in the path string of the root API. The default is `'/'`, which will mount to the root path.
27+
* `auto_reload`: set to `True` to enable service auto reload when the code changed during debug, default is None.
2728

2829
When you initialize UtilMeta, you can also import the UtilMeta service instance of the current process in this way
2930

@@ -393,7 +394,23 @@ UtilMeta provides a `DjangoSettings` configuration that provides declarative Dja
393394

394395
* `secret_key`: Specifies the **secret key** for Django. It is recommended to generate a long random key in the environment variable.
395396
* `apps`: for specifying Django `INSTALLED_APPS`.
396-
* `apps_package`: This is a convenient configuration item. If your installed apps are all in a package, you can use `apps_package` to specify the path of the package. UtilMeta will read all the subfolders in it to find the Django app.
397+
* `apps_package`: If your django apps are defined under some folder, such as:
398+
``` hl_lines="3"
399+
/project
400+
/config
401+
/domain
402+
/app1
403+
/migrations
404+
models.py
405+
/app2
406+
/migrations
407+
models.py
408+
```
409+
410+
You can specify `apps_package='domain'` to easily recognize all apps under the `/domain` folder, current approach is to detect all subfolders with `migrations` folder.
411+
412+
If the are multiple packages that contains apps, you can pass in a list like `apps_package=['domain.apps', 'vendors']`
413+
397414
* `middleware`: You can pass in a list of Django middleware here.
398415
* `module_name`: Specifies the configuration file reference for Django.
399416
* `extra`: You can pass in a dict to specify additional Django configuration.

docs/en/guide/migration.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,61 @@ if __name__ == "__main__":
200200

201201
Just take the result of `API.__as__` as a route of `tornado.web.Application`
202202

203-
### Integration rules
203+
### API Integration rules
204204

205205
When integrate UtilMeta to other existing projects, you should only integrate **One** API classes. If you develop other API classes, you can use API mounting as a subroute of the integrated API classes.
206206

207207
Because the service is not controlled by UtilMeta when the UtilMeta API integrate other projects, so `API.__as__` will create a hidden UtilMeta service for control, in order to avoid service conflicts, you can only call the `API.__as__` function once.
208208

209+
### Configure UtilMeta service
210+
211+
You can declare a UtilMeta service instance to inject your configurations. Set the API class you defined as root API of service using `api` param, then replace the `__as__` method to `service.adapt` to adapt the entire service, like:
212+
213+
```python hl_lines="34"
214+
import django
215+
from django.urls import re_path
216+
from django.http.response import HttpResponse
217+
from utilmeta.core import api, response
218+
219+
class TimeAPI(api.API):
220+
class response(response.Response):
221+
result_key = 'data'
222+
message_key = 'msg'
223+
224+
@api.get
225+
def now(self):
226+
return self.request.time
227+
228+
def django_test(request, route: str):
229+
return HttpResponse(route)
230+
231+
from utilmeta import UtilMeta
232+
from utilmeta.conf import Time
233+
234+
service = UtilMeta(
235+
__name__,
236+
name='time',
237+
backend=django,
238+
api=TimeAPI,
239+
)
240+
service.use(Time(
241+
datetime_format="%Y-%m-%d %H:%M:%S",
242+
use_tz=False
243+
))
244+
245+
urlpatterns = [
246+
re_path('test/(.*)', django_test),
247+
service.adapt('/api/v1/time')
248+
]
249+
```
250+
`service.adapt` takes a route param to specify the adapted route that will prepend to the UtilMeta service root url. Using the above method, when request `/api/v1/time/now`, you will see the datetime string using the configured format:
251+
```json
252+
{"data": "2025-04-15 16:38:30", "msg": ""}
253+
```
254+
255+
!!! note
256+
The `backend` param of UtilMeta service instance should be consistent to the framework the project using.
257+
209258
## Integrate other frameworks
210259

211260
Your UtilMeta project can also integrate other framework's APIs

docs/en/guide/ops.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,9 @@ meta connect
253253
You can see that the browser opened a window of UtilMeta platform, where you can see the APIs, Data tables, log and monitoring of your service.
254254

255255
<img src="https://utilmeta.com/assets/image/connect-local-api.png" href="https://ops.utilmeta.com" target="_blank" width="800"/>
256+
!!! warning "Cookie"
257+
When you connecting local API directly, it is not possible to directly send **cookies** from the browser to API service (because the UtilMeta platform is cross domain with local service). Connecting to online public services will not have this problem because requests can be forwarded through the UtilMeta platform. In the future, UtilMeta platform will launch browser plugin and desktop client to solve the problem. Currently, you can use [UtilMeta's declarative web client](../client) to write debugging scripts and test cases for local APIs with cookies.
258+
256259
### Connect Public Service
257260

258261
Connecting to the API service deployed online with public network address requires you to register an account on the UtilMeta platform. Because the management of online services requires a stricter authorization and authentication mechanism, you need to create a project team on the UtilMeta platform first. When you enter an empty project team, You can see the connection prompt for the UtilMeta platform

docs/zh/guide/config-run.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ UtilMeta 服务的第一个参数接收当前模块的名称(`__name__`),
2424
* `asynchronous`:强制指定服务是否提供异步接口,默认由运行时框架的特性决定
2525
* `api`:传入 UtilMeta 的根 API 类或它的引用字符串
2626
* `route`:传入 UtilMeta 的根 API 挂载的路径字符串,默认为 `'/'`,即挂载到根路径
27+
* `auto_reload`: 设为 `True` 可以启用服务在代码更新时的自动重载(不建议用于生产),默认未开启
2728

2829
当你初始化 UtilMeta 后,除了直接导入外,你还可以用这种方式导入当前进程的 UtilMeta 服务实例
2930

@@ -420,12 +421,29 @@ env = ServiceEnvironment(file='/path/to/config.json')
420421
UtilMeta 提供了一个 `DjangoSettings` 配置,可以为所有使用 Django 作为 `backend` 的项目和使用 django ORM 的项目提供声明式的 django 配置, `DjangoSettings` 的常用配置参数如下
421422

422423
* `secret_key`:指定 Django 项目的密钥,推荐在环境变量中生成一个长的随机密钥
423-
* `apps`:用于指定 Django 的 `INSTALLED_APPS`
424-
* `apps_package`:这是一个便捷配置项,如果你的已安装 app 都放在一个包中,你可以使用 `apps_package` 指定这个包的路径,UtilMeta 会读取其中的所有子文件夹查找 django app
424+
* `apps`:可以传入一个包列表用于直接指定 Django 的 `INSTALLED_APPS`
425+
* `apps_package`:如果你的 django app 都定义在某个文件夹中,例如:
426+
``` hl_lines="3"
427+
/project
428+
/config
429+
/domain
430+
/app1
431+
/migrations
432+
models.py
433+
/app2
434+
/migrations
435+
models.py
436+
```
437+
438+
你可以指定 `apps_package='domain'` 来便捷地识别 `/domain` 中所有的 app,目前的识别方式是检测其中是否包括 `migrations` 文件夹(即 django 用于保存 app 中数据模型变更记录的文件夹)
439+
440+
如果有多个这样的包你还可以传入一个包的相对路径的列表,如 `apps_package=['domain.apps', 'vendors']`
441+
425442
* `middleware`:可以传入一个 django 中间件的列表
426443
* `module_name`:指定 django 的配置文件引用
427444
* `extra`:可以传入一个字典指定额外的 Django 配置
428445

446+
429447
另外,如果你没有为 `DjangoSettings` 指定 `module_name`,它将默认使用 UtilMeta 服务所在的模块作为配置,所以你也可以在服务 `setup()` **之前** 直接在这个文件中声明 django 配置,用法与原生 django 配置一样,例如
430448

431449
```python

docs/zh/guide/migration.md

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,65 @@ if __name__ == "__main__":
201201

202202
就是将 `API.__as__` 的结果作为 `tornado.web.Application` 的一条路由
203203

204-
### UtilMeta 接入规则
204+
### API 接入规则
205205

206206
在将 UtilMeta 接入其他的现有项目时,你应该只接入 **一个** API 类,如果你开发了其他的 API 类,那么可以使用挂载作为接入的 API 类的子路由
207207

208208
因为在 UtilMeta 接口接入其他项目的时候,服务不是由 UtilMeta 控制的,所以 `API.__as__` 函数会创建一个 隐藏的 UtilMeta 服务进行调控,所以为了避免服务冲突,你只能调用一次 `API.__as__` 函数
209209

210+
### 配置 UtilMeta 服务
211+
212+
如果你需要对 UtilMeta 接口进行额外的配置,你可以直接声明一个 UtilMeta 服务实例来注入配置,把定义好的 API 类通过 `api` 参数指定为服务的根 API,然后可以使用 `service.adapt` 方法代替 `__as__` 方法进行服务的适配,例如
213+
214+
```python hl_lines="34"
215+
import django
216+
from django.urls import re_path
217+
from django.http.response import HttpResponse
218+
from utilmeta.core import api, response
219+
220+
class TimeAPI(api.API):
221+
class response(response.Response):
222+
result_key = 'data'
223+
message_key = 'msg'
224+
225+
@api.get
226+
def now(self):
227+
return self.request.time
228+
229+
def django_test(request, route: str):
230+
return HttpResponse(route)
231+
232+
from utilmeta import UtilMeta
233+
from utilmeta.conf import Time
234+
235+
service = UtilMeta(
236+
__name__,
237+
name='time',
238+
backend=django,
239+
api=TimeAPI,
240+
)
241+
service.use(Time(
242+
datetime_format="%Y-%m-%d %H:%M:%S",
243+
use_tz=False
244+
))
245+
246+
urlpatterns = [
247+
re_path('test/(.*)', django_test),
248+
service.adapt('/api/v1/time')
249+
]
250+
```
251+
252+
`service.adapt` 方法接收一个路径参数,可以指定服务挂载的路径,如果 UtilMeta 服务定义了根路径,`adapt` 方法的挂载路径会加在服务根路径的前方
253+
254+
使用这样的适配方式,当请求路径 `/api/v1/time/now` 时,就可以看到按照配置格式序列化的当前时间,如
255+
```json
256+
{"data": "2025-04-15 16:38:30", "msg": ""}
257+
```
258+
259+
!!! note
260+
声明的 UtilMeta 服务实例所使用的 `backend` 需要与当前项目所使用的框架一致
261+
262+
210263
## 接入其他框架接口
211264

212265
你的 UtilMeta 项目也可以接入其他框架开发好的接口,比如

docs/zh/guide/ops.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,11 @@ meta connect
256256
UtilMeta 版本 2.6.2 及以上支持这个用法
257257

258258
即可看到浏览器中打开了 UtilMeta 管理平台的窗口,你可以在其中看到你服务的 API,数据表,日志和监控等
259+
259260
<img src="https://utilmeta.com/assets/image/connect-local-api.png" href="https://ops.utilmeta.com" target="_blank" width="800"/>
261+
!!! warning "Cookie"
262+
在本地直连(以及内网服务直连)模式下,无法直接从浏览器发送 Cookie 到 API 服务(因为 UtilMeta 平台与本地服务是跨域的),连接线上公网服务则不会有这样的问题因为请求可以通过 UtilMeta 平台进行转发,后续 UtilMeta 平台会上线浏览器插件和桌面版客户端解决直连服务的 Cookie 调试问题,目前你可以使用 [UtilMeta 的声明式 Web 客户端](../client) 来编写调试脚本和测试用例
263+
260264
### 连接线上服务
261265

262266
连接在线上部署的,提供网络访问地址的 API 服务需要你进入 UtilMeta 平台注册一个账号,因为管理线上服务需要更严格的授权与鉴权机制,所以需要你在 UtilMeta 平台中先创建一个项目团队,进入空的项目团队中时,你可以看到 UtilMeta 平台的连接提示

tests/test_0_basic/test_cache.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,18 @@ def test_cache(self, service):
1616
cache.pop('key')
1717
assert cache.get('key') is None
1818

19+
def test_aioredis(self):
20+
from utilmeta.core.cache.backends.redis.aioredis import AioredisAdaptor
21+
aioredis = AioredisAdaptor.load_aioredis()
22+
assert aioredis.from_url
23+
1924
# @pytest.mark.asyncio
2025
# async def test_async_cache(self):
2126
# from utilmeta.core.cache import Cache
2227
# cache = Cache(
2328
# engine='memory'
2429
# )
30+
# cache.apply('default', asynchronous=True)
2531
# assert await cache.aget('key') is None
2632
# await cache.aset('key', '123')
2733
# assert await cache.aget('key') == '123'

tests/test_7_ops/django_site/django_demo/urls.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,45 @@ def add(request, a: int, b: int):
3030
return {"result": a + b}
3131

3232

33+
import django
34+
from django.http.response import HttpResponse
35+
from utilmeta.core import api, response
36+
37+
38+
class TimeAPI(api.API):
39+
class response(response.Response):
40+
result_key = 'data'
41+
message_key = 'msg'
42+
43+
@api.get
44+
def now(self):
45+
return self.request.time
46+
47+
48+
def django_test(request, route: str):
49+
return HttpResponse(route)
50+
51+
52+
from utilmeta import UtilMeta
53+
from utilmeta.conf import Time
54+
55+
service = UtilMeta(
56+
__name__,
57+
name='time',
58+
backend=django,
59+
api=TimeAPI,
60+
)
61+
service.use(Time(
62+
datetime_format="%Y-%m-%d %H:%M:%S",
63+
use_tz=False
64+
))
65+
66+
3367
# Wire up our API using automatic URL routing.
3468
# Additionally, we include login URLs for the browsable API.
3569
urlpatterns = [
3670
path('', include(router.urls)),
3771
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
3872
path("api-ninja/", ninja_api.urls),
73+
service.adapt('/api/v1/time')
3974
]

tests/test_7_ops/test_ops.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import datetime
2+
13
import pytest
24
from tests.conftest import make_cmd_process, db_using
35
from utilmeta.core import cli
@@ -109,6 +111,12 @@ def test_django_operations(self, django_wsgi_process):
109111
data = add.data
110112
assert isinstance(data, dict) and data.get('result') == 3
111113

114+
now = client.get('/api/v1/time/now')
115+
assert now.status == 200
116+
assert isinstance(now.data, dict)
117+
assert isinstance(datetime.datetime.strptime(
118+
now.data.get('data'), '%Y-%m-%d %H:%M:%S'), datetime.datetime)
119+
112120
def test_django_asgi_operations(self, django_asgi_process):
113121
time.sleep(OPS_WAIT)
114122
with OperationsClient(

0 commit comments

Comments
 (0)