Skip to content

Commit 26222bd

Browse files
committed
add [meta check] command and fallback for help command, add resolve_domain function with default_dns_resolve_timeout in Preference, support serialize for manage=False django models (such as database view), remove base_url private restriction for OperationsAPI, add error handling for field rule generation, optimize @service.mount decorator
1 parent 1143261 commit 26222bd

22 files changed

Lines changed: 366 additions & 44 deletions

File tree

docs/en/community/release.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
# Release Note
22

3+
## v2.7.5
4+
5+
Release Date: 2025/3/21
6+
7+
### New features
8+
9+
* ·Operations support `connection_key` to authorize the local and private services, and support connecting to private service directly after `connection_key` and `private_scope` are set.
10+
* Add `meta check` command to check if UtilMeta can load normally (no error)
11+
12+
### Optimized
13+
14+
* Optimized `orm.Field` to support lookup that has conflicted name with field name.
15+
* Optimized runtime dispatch for all Adaptors (request, response, file, ...) with `__backends_package__` specified.
16+
* Optimized ResponseFile's file name recognition.
17+
* Optimized Session's `save` for database drivin session.
18+
* Optimized Filter's `query` to handle `@classmethod`.
19+
* orm support query and serialize `managed=False` Django model (such as the database view model without primary key)
20+
21+
### Fixed
22+
23+
* Fix Django route mounting problem (`__as__` method)
24+
325
## v2.7.4
426

527
Release Date: 2025/2/10

docs/en/guide/ops.md

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ The additional specified OpenAPI document will be integrated with the automatica
233233
## Connect to UtilMeta Platform
234234

235235
UtilMeta provides a platform for the observation and management operations of the API services: [UtilMeta Platform ](https://ops.utilmeta.com) you can enter the platform to connect and manage your UtilMeta service (or other services with supported frameworks), view API documents, data, logs and monitoring.
236-
### Connect Local Node
236+
### Connect Local API
237237

238238
If you have configured the Operations successfully and run the local service, you can see the following prompt
239239

@@ -253,7 +253,7 @@ 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-
### Connect Public service
256+
### Connect Public Service
257257

258258
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
259259

@@ -269,7 +269,7 @@ please visit [URL] to view and manage your APIs'
269269

270270
Click the URL to access the online service you have connected in UtilMeta Platform, or click the **I’ve executed successfully** button in the platform to refresh after successful execution
271271

272-
### Connect Intranet Cluster
272+
### Connect Intranet Cluster (by proxy)
273273

274274
In addition to manage API services on the public network, we sometimes need to manage API services in private network clusters, such as internal services within the company's intranet, which do not have public IP addresses or access URLs. To manage these internal network services, we need to set up public network proxies in the internal network cluster, deploy a proxy service node for internal network penetration with authentication and service registration.
275275

@@ -316,6 +316,60 @@ service.use(Operations(
316316
!!! note
317317
UtilMeta services in the cluster should share the same Operations database as the proxy service, this database will serve as the service authentication basis and management storage center in the cluster. Current version mainly support PostgreSQL and MySQL as operations storage, futher releases will support other storage vendors.
318318

319+
### Connect Private Service (directly)
320+
321+
Beside the proxy mode in the above section, UtilMeta also provided a method to connect service node in the private network directly, similiar to connecting local API, with two conditions required:
322+
323+
* Your management client (the computer that opened the UtilMeta Platform) must located at the **same private network** with the service you are going to manage. which means your computer can directly access the private service's API.
324+
* Your private service should provide HTTPS access (with SSL certificates)
325+
326+
!!! note
327+
In the site that served with HTTPS (i.e. UtilMeta platform), browser will block all http requests (except for localhost), as you can see in the [Mixed Content | MDN](https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content), so to connect to the private service directly, you should use the HTTPS protocol
328+
329+
After meeting the above conditions, you need to add two additional configurations for Operations:
330+
331+
* `connection_key`: In order to ensure the security of private network nodes, directly connecting to private network nodes requires you to pre-set a key (preferably a long random string) in the Operations configuration. UtilMeta will first verify this key for management requests from the private network, and only execute the corresponding request after verification is successful.
332+
* `private_scope`: Authorize the permission range for private network requests. The value of the permission range can refer to the introduction in the `'local_scope'` section above. If you need to grant all permissions, you can use `['*']`
333+
334+
The configuration example is as follows:
335+
```python
336+
from config.env import env
337+
from utilmeta.ops import Operations
338+
339+
service.use(Operations(
340+
base_url='https://my-private-service.com/api', # replace with your domain
341+
# other settings...
342+
connection_key=env.CONNECTION_KEY,
343+
private_scope=['*']
344+
))
345+
```
346+
347+
Noted that `base_url` should also need to set to the address with HTTPS protocol and domain name.
348+
349+
!!! note
350+
Connect to private network service directly requres UtilMeta >= 2.7.5
351+
352+
After the configuration, restart your service and you will see the OperationsAPI URL in the output
353+
354+
```
355+
UtilMeta OperationsAPI loaded at https://my-private-service.com/api/ops
356+
```
357+
358+
Then we can login to [UtilMeta Platform](https://ops.utilmeta.com), enter your project team, click the **\[Connect Local API\]** of **\[+\]** button on the top bar
359+
360+
<img src="https://utilmeta.com/assets/image/connect-local-hint.png" href="https://ops.utilmeta.com" target="_blank" width="300"/>
361+
362+
Enter the OperationsAPI URL of your service in the prompted dialog, (for example: `https://my-private-service.com/api/ops`)
363+
364+
<img src="https://utilmeta.com/assets/image/connect-local-api-form.png" href="https://ops.utilmeta.com" target="_blank" width="500"/>
365+
366+
If you connected successfully, UtilMeta platform will prompt you to enter the `connection_key` you configured in the service, enter the correct key then you will be connected to your private service directly
367+
368+
<img src="https://utilmeta.com/assets/image/set-connection-key-en.png" href="https://ops.utilmeta.com" target="_blank" width="500"/>
369+
370+
!!! note
371+
With "Save the key" checked, you can save the `connection_key` and OperationsAPI address of the private service to your project team, you won't need to enter these info again to connect to this service next time.
372+
319373
## Connect to Python project
320374

321375
The operations system of UtilMeta framework can not only connect to the services made by UtilMeta framework, but also connect to the existing Python backend projects. The currently supported frameworks include

docs/zh/community/release.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
## v2.7.5
44

5+
发布时间:2025/3/21
6+
57
### 新特性
68

79
* ·Operations 系统支持配置 `connection_key` 对本地或内网的直连管理模式请求进行鉴权,配置 `connection_key``private_scope` 参数后可以在 UtilMeta 管理平台中直接管理与客户端位于同一内网的 UtilMeta 服务
10+
* 新增 `meta check` 命令用于检测 UtilMeta 服务是否加载正常(启动无错误)
811

912
### 优化项
1013

@@ -13,6 +16,7 @@
1316
* 优化 File 文件对于 ResponseFile (HTTP 响应作为文件) 的文件名识别
1417
* 优化数据库驱动的 Session 的保存(`save`) 行为
1518
* 优化 Filter 组件的 `query` 查询函数对于 `@classmethod` 类方法的处理
19+
* orm 支持查询和序列化 `managed=False` 的 Django 模型(如没有主键的数据库视图)
1620

1721
### 问题修复
1822

docs/zh/guide/ops.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -326,10 +326,10 @@ service.use(Operations(
326326
除了上文的连接管理整个内网集群的 **代理模式** 外,UtilMeta 还提供了一种类似于连接本地节点的方式直接管理内网中的服务节点,这种直连内网服务管理需要满足两个条件:
327327

328328
* 你的客户端(你打开 UtilMeta 管理平台的浏览器的电脑)与你要管理的 UtilMeta 服务位于同一 **内网**,也就是说你的电脑可以直接通过服务的内网地址直接访问到服务的 API 接口
329-
* 你的内网服务节点需要提供 HTTPS 协议的访问
329+
* 你的内网服务节点需要提供 HTTPS 协议的访问(需要配置 SSL 证书)
330330

331331
!!! note
332-
对 HTTPS 协议的要求是浏览器对每一个使用 HTTPS 协议服务的网页(UtilMeta 管理平台)所调用任何除了本机地址(localhost / 127.0.0.1)外的 URL 的要求,之后 UtilMeta 也规划上线桌面客户端或浏览器插件,可以绕过这个限制
332+
对 HTTPS 协议的要求是浏览器对每一个使用 HTTPS 协议服务的网页(UtilMeta 管理平台)所调用任何除了本机地址(localhost / 127.0.0.1)外的 URL 的要求(参考文档:[Mixed Content | MDN](https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content),之后 UtilMeta 也规划上线桌面客户端或浏览器插件,可以绕过这个限制
333333

334334
满足以上条件后,直连内网节点需要你对 Operations 增加两项配置:
335335

@@ -349,19 +349,32 @@ service.use(Operations(
349349
))
350350
```
351351

352+
其中 `base_url` 也需要设置为使用 HTTPS 协议和域名的服务地址
353+
352354
!!! note
353355
使用直连内网节点的功能需要 UtilMeta 框架 2.7.5 及以上的版本
354356

355-
配置完成后重启服务你会看到输出中有类似本地服务的
357+
配置完成后重启服务你会看到输出中包括 OperationsAPI 的地址
356358

357359
```
358-
UtilMeta OperationsAPI loaded at https://my-private-service.com/api/ops, connect your APIs at https://ops.utilmeta.com/localhost?local_node=https://my-private-service.com/api/ops
360+
UtilMeta OperationsAPI loaded at https://my-private-service.com/api/ops
359361
```
360362

361-
直接点击第二个链接即可进入 UtilMeta 平台连接你的内网节点,进入后会弹出一个对话框让你输入之前设置好的 `connection_key`,输入正确的密钥即可连接并管理内网节点
363+
之后我们进入 [UtilMeta 管理平台](https://ops.utilmeta.com),进入你创建的团队中,点击顶栏中的【+】按钮中的【Connect Local API】连接内网服务
364+
365+
<img src="https://utilmeta.com/assets/image/connect-local-hint.png" href="https://ops.utilmeta.com" target="_blank" width="300"/>
366+
367+
在点击打开的对话框中输入服务的 OperationsAPI 地址(例如 `https://my-private-service.com/api/ops`
368+
369+
<img src="https://utilmeta.com/assets/image/connect-local-api-form.png" href="https://ops.utilmeta.com" target="_blank" width="500"/>
370+
371+
连接成功后会提示你输入之前设置好的 `connection_key`,输入正确的密钥即可连接并管理内网节点
362372

363373
<img src="https://utilmeta.com/assets/image/set-connection-key-en.png" href="https://ops.utilmeta.com" target="_blank" width="500"/>
364374

375+
!!! note
376+
勾选保存即可将 `connection_key` 与内网服务地址保存到这个项目团队中,后面再次连接就不需要重复输入了
377+
365378

366379
## 连接现有 Python 项目
367380

tests/server/api.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,3 +412,11 @@ def hello(self):
412412
@api.get
413413
def plus(self, a: int, b: int) -> int:
414414
return a + b
415+
416+
class fQuery(utype.Schema):
417+
f: str
418+
419+
@api.get
420+
def file(self, fq: fQuery = request.Query):
421+
from utilmeta.core.file import File
422+
return self.response(attachment=File(fq.f.encode(), filename='f.txt'))

tests/server/app/migrations/0001_initial.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ class Migration(migrations.Migration):
185185
model_name="basecontent",
186186
name="liked_bys",
187187
field=models.ManyToManyField(
188-
db_table="like", related_name="likes", to="app.user"
188+
db_table="liked", related_name="likes", to="app.user"
189189
),
190190
),
191191
migrations.AlterUniqueTogether(
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Generated by Django 4.2.17 on 2025-03-19 03:13
2+
3+
from django.db import migrations, models
4+
5+
6+
CREATE_SQL = """
7+
CREATE VIEW article_stats AS
8+
SELECT
9+
a.basecontent_ptr_id AS article_id,
10+
COUNT(DISTINCT l.user_id) AS liked_bys_num,
11+
COUNT(DISTINCT c.basecontent_ptr_id) AS comments_num
12+
FROM
13+
article a
14+
INNER JOIN
15+
content ct ON a.basecontent_ptr_id = ct.id
16+
LEFT JOIN
17+
liked l ON ct.id = l.basecontent_id
18+
LEFT JOIN
19+
comment c ON ct.id = c.on_content_id
20+
GROUP BY
21+
a.basecontent_ptr_id;
22+
"""
23+
24+
DROP_SQL = "DROP VIEW article_stats;"
25+
26+
27+
class Migration(migrations.Migration):
28+
29+
dependencies = [
30+
("app", "0002_article_tags"),
31+
]
32+
33+
operations = [
34+
migrations.CreateModel(
35+
name="Statistics",
36+
fields=[
37+
(
38+
"id",
39+
models.BigAutoField(
40+
auto_created=True,
41+
primary_key=True,
42+
serialize=False,
43+
verbose_name="ID",
44+
),
45+
),
46+
("date", models.DateField()),
47+
("articles_num", models.PositiveIntegerField(default=0)),
48+
("comments_num", models.PositiveIntegerField(default=0)),
49+
("users_num", models.PositiveIntegerField(default=0)),
50+
],
51+
options={
52+
"db_table": "stats",
53+
"managed": False,
54+
},
55+
),
56+
migrations.RunSQL(
57+
sql=CREATE_SQL,
58+
reverse_sql=DROP_SQL,
59+
),
60+
]

tests/server/app/models.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class BaseContent(AwaitableModel):
4646
created_at = models.DateTimeField(auto_now_add=True)
4747
updated_at = models.DateTimeField(auto_now=True)
4848
public = models.BooleanField(default=False)
49-
liked_bys = models.ManyToManyField(User, related_name="likes", db_table="like")
49+
liked_bys = models.ManyToManyField(User, related_name="likes", db_table="liked")
5050
author = models.ForeignKey(User, related_name="contents", on_delete=models.CASCADE)
5151
author_id: int
5252
type = models.CharField(max_length=20, default="article")
@@ -88,3 +88,13 @@ class Session(AbstractSession):
8888

8989
class Meta:
9090
db_table = 'session'
91+
92+
93+
class ArticleStats(AwaitableModel):
94+
article_id = models.PositiveIntegerField()
95+
comments_num = models.PositiveIntegerField(default=0)
96+
liked_bys_num = models.PositiveIntegerField(default=0)
97+
98+
class Meta:
99+
db_table = "article_stats"
100+
managed = False

tests/server/app/schema.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
from utype.types import *
22
from utilmeta.core import orm, auth
3-
from .models import User, Article, Comment, BaseContent
3+
from .models import User, Article, Comment, BaseContent, ArticleStats
44
from utype import Field
55
from utilmeta.core.orm.backends.django import expressions as exp
66
from utilmeta.utils import awaitable
77
from django.db import models
88

99

10-
__all__ = ["UserSchema", "ArticleSchema", "CommentSchema",
10+
__all__ = ["UserSchema", "ArticleSchema", "CommentSchema", "ArticleStatsSchema", "ArticleStatsQuery",
1111
"ContentSchema", 'UserBase', 'UserQuery', 'ArticleQuery', 'ArticleBase', 'ContentBase']
1212

1313

@@ -279,3 +279,16 @@ class ArticleQuery(orm.Query[Article]):
279279
offset: int = orm.Offset(alias='@offset')
280280
limit: int = orm.Limit(alias='@limit')
281281
scope: dict = orm.Scope()
282+
283+
284+
class ArticleStatsQuery(orm.Query[ArticleStats]):
285+
article_id: int
286+
comments_gte: int = orm.Filter(query=lambda x: models.Q(comments_num__gte=x))
287+
liked_bys_gte: int = orm.Filter(query=lambda x: models.Q(liked_bys_num__gte=x))
288+
sort: str = orm.OrderBy([ArticleStats.article_id, ArticleStats.liked_bys_num, ArticleStats.comments_num])
289+
290+
291+
class ArticleStatsSchema(orm.Schema[ArticleStats]):
292+
article_id: int
293+
comments_num: int
294+
liked_bys_num: int

tests/test_1_orm/test_schema_query.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,42 @@ def test_init_articles(self, service, db_using):
115115
assert content.id == 1
116116
assert content.article.id == 1
117117

118+
def test_unmanaged_view_model_query(self, service, db_using):
119+
from app.schema import ArticleStatsSchema, ArticleStatsQuery
120+
res1 = ArticleStatsSchema.serialize(
121+
ArticleStatsQuery(comments_gte=2, sort='liked_bys_num'),
122+
context=orm.QueryContext(using=db_using)
123+
)
124+
assert len(res1) == 2
125+
assert res1[0].article_id == 4
126+
assert res1[1].article_id == 1
127+
assert res1[1].comments_num == 2
128+
assert res1[1].liked_bys_num == 3
129+
res = ArticleStatsSchema.init(
130+
ArticleStatsQuery(article_id=1),
131+
context=orm.QueryContext(using=db_using)
132+
)
133+
assert res.article_id == 1
134+
assert res.comments_num == 2
135+
136+
@pytest.mark.asyncio
137+
async def test_async_unmanaged_view_model_query(self, service, db_using):
138+
await self.refresh_db(db_using)
139+
from app.schema import ArticleStatsSchema, ArticleStatsQuery
140+
res1 = await ArticleStatsSchema.aserialize(
141+
ArticleStatsQuery(comments_gte=2, sort='liked_bys_num'), context=orm.QueryContext(using=db_using)
142+
)
143+
assert len(res1) == 2
144+
assert res1[0].article_id == 4
145+
assert res1[1].article_id == 1
146+
assert res1[1].comments_num == 2
147+
assert res1[1].liked_bys_num == 3
148+
res = await ArticleStatsSchema.ainit(
149+
ArticleStatsQuery(article_id=1), context=orm.QueryContext(using=db_using)
150+
)
151+
assert res.article_id == 1
152+
assert res.comments_num == 2
153+
118154
def test_orm_preferences(self, service):
119155
# orm_on_conflict_annotation
120156

@@ -204,6 +240,7 @@ def test_queryset_generator(self, service, db_using):
204240

205241
@pytest.mark.asyncio
206242
async def test_async_queryset_generator(self, service, db_using):
243+
await self.refresh_db(db_using)
207244
from app.models import Article
208245
from app.schema import ArticleQuery
209246
from django.db import models

0 commit comments

Comments
 (0)