Skip to content

Commit f8cb22f

Browse files
committed
add orm.Search as search param, support request context vars in orm.Schema, add --split to meta gen_openapi
1 parent 363fd0e commit f8cb22f

31 files changed

Lines changed: 1011 additions & 384 deletions

File tree

docs/en/community/release.md

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

3+
## v2.8.0
4+
5+
Release Date: 2025/9/10
6+
7+
### New features
8+
9+
* API support **Server-Sent Events (SSE)**: [Use SSE in API](../../guide/api-route#server-sent-events)
10+
* Clien support handling Server-Sent Events (SSE) response: [Handle SSE Response](../../guide/client#server-sent-events)
11+
* `@api` decorator added `timeout` parameter to specifiy the timeout for API function processing time or Client request timeout.
12+
* Added HTTP cache plugin: `api.Cache`: can handle HTTP caching like `Last-Modifed` / `Etag` automatically
13+
* Added search parameter `orm.Search`, support various search mode for multiple model fields: [Search Param](../../guide/schema-query/#search-param)
14+
* `orm.Schema` support defining request context variables (such as current request user / IP) and query request context fields by passing the current request object to `context` param of the serialize methods: [Use context vars in query function](../../guide/schema-query/#relational-query-function)
15+
* `meta gen_openapi` support `--split` parameter that spliting the API document in seperate files by endpoint, convenient for AI tools to parse and read
16+
17+
### Optimized
18+
19+
* Optimize `orm.Field` field parsing and rule merging.
20+
* Client reuse request session (`httpx.Client` / `aiohttp.Session` / `requests.Session`) in `with` and `async with` block.
21+
22+
### Fixed
23+
24+
* Fix YAML output format of generated OpenAPI document.
25+
326
## v2.7.6
427

528
Release Date: 2025/6/6

docs/en/community/roadmap.md

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

33
## Long term planing
44

5-
### WebSocket / SSE
5+
### WebSocket
66

7-
* Support writing WebSocket / SSE / Server Push APIs in declarative way
8-
* Support document generation and testing for WebSocket / SSE APIs
7+
* Support writing WebSocket APIs in declarative way
8+
* Support document generation and testing for WebSocket APIs
99

1010
### GraphQL
1111

@@ -31,7 +31,7 @@
3131

3232
## Version roadmap
3333

34-
### v2.8
34+
### v2.9
3535

3636
**New Features**
3737

docs/en/guide/schema-query.md

Lines changed: 84 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -401,35 +401,8 @@ The pre-condition for `queryset` parameter is to specify a **Multiple Relation N
401401
!!! warning "No Slicing"
402402
Please **DO NOT** slice the specified `queryset` (such as limiting the number of returned results), because in order to optimize N+1 query problems, The query implementation of `queryset` is to query all relational objects at once and distribute them according to their corresponding relationships. If you slice the queryset, the list of relational objects assigned to the queried instances may be incomplete. If you need to implement a requirement similar to "querying up to N relational objects per instance", please refer to the **relational query function** below.
403403

404-
### Relational query function
405-
406-
Relational query function provides a hook that can be customized. You can write any condition for the relational query, such as adding filter and sort conditions, controlling the quantity, etc. The relational query function can be declared in the following ways
407-
408-
#### Single primary-key function
409-
The function accepts a single primary key of the target queryset as input, and returns the related queryset. Let’s take a requirement as an example: we need to query a list of users, each user needs to attach **two most liked articles**. The code example for implementation is as follows
410-
411-
```python hl_lines="8-12"
412-
class ArticleSchema(orm.Schema[Article]):
413-
id: int
414-
content: str
415-
416-
class UserSchema(orm.Schema[User]):
417-
username: str
418-
top_2_articles: List[ArticleSchema] = orm.Field(
419-
lambda user_id: Article.objects.annotate(
420-
favorites_num=models.Count('favorited_bys')
421-
).filter(
422-
author_id=user_id
423-
).order_by('-favorites_num')[:2]
424-
)
425-
```
426-
427-
In this example, the `top_2_articles` field of UserSchema specifies a relational query function, which accepts a primary key value of the target user and returns the corresponding article queryset. UtilMeta will complete the serialization and result distribution according to the type annotation ( `List[ArticleSchema]`) of the field
428-
429-
**Optimized compression of a single relationship object**
430-
431-
Looking at the above example, we can clearly see that in order to get the conditional relation value of the target, the query in the function needs to run N times, N is the length of the target queryset, so what can be compressed into a single query?
432-
The answer is that when you only need to query **1** of the target relational object, you can directly declare the queryset, and UtilMeta will process it into a **subquery** to compress it into a single query, such as
404+
#### Single relationship object
405+
when you only need to query **1** of the target relational object, you can directly declare the queryset, and UtilMeta will process it into a **subquery** to compress it into a single query, such as
433406

434407
```python hl_lines="8-12"
435408
class ArticleSchema(orm.Schema[Article]):
@@ -449,37 +422,44 @@ class UserSchema(orm.Schema[User]):
449422

450423
!!! tip "OuterRef"
451424
Django uses `OuterRef` to reference the outer fields, in the example, we referenced the primary key of the target User model
425+
### Relational query function
426+
427+
Relational query function provides a hook that can be customized. You can write any condition for the relational query, such as adding filter and sort conditions, controlling the quantity, etc. The relational query function can be declared in the following ways
428+
429+
Let’s take a requirement as an example. Suppose we need to query a list of users, in which each user needs to attach “**Followers the current request user knows**”, which is a common requirement in social media such as Twitter (X). so the requirement can be simply and efficiently implemented by using the primary key list function.
452430

453-
#### Primary-Key List Function
454-
Let’s take another requirement as an example. Suppose we need to query a list of users, in which each user needs to attach “**Followers the current request user knows**”, which is a common requirement in social media such as Twitter (X). so the requirement can be simply and efficiently implemented by using the primary key list function.
431+
```python hl_lines="7 17"
432+
from utilmeta.core import orm, request
455433

456-
```python hl_lines="16"
457434
class UserSchema(orm.Schema[User]):
458435
username: str
459436

460437
@classmethod
461-
def get_runtime_schema(cls, user_id):
462-
def get_followers_you_known(*pks):
463-
mp = {}
464-
for val in User.objects.filter(
465-
followings__in=pks,
466-
followers=user_id
467-
).values('followings', 'pk'):
468-
mp.setdefault(val['followings'], []).append(val['pk'])
469-
return mp
470-
471-
class user_schema(cls):
472-
followers_you_known: List[cls] = orm.Field(get_followers_you_known)
473-
474-
return user_schema
438+
def get_followers_you_known(cls, *pks, user_id: int = request.var.user_id):
439+
mp = {}
440+
for val in User.objects.filter(
441+
followings__in=pks,
442+
followers=user_id
443+
).values('followings', 'pk').using(db_using):
444+
mp.setdefault(val['followings'], []).append(val['pk'])
445+
return mp
446+
447+
followers_you_known: List[UserBase] = orm.Field(
448+
get_followers_you_known,
449+
default_factory=list
450+
)
475451
```
476452

477453
In the example, `UserSchema` defines a class function that generate different queries for different requesting users, in which we define a `get_followers_you_known` query function that accepts a list of **queried primary keys** of current Schema instances and constructs a dict that map each key with a primary key list of the **target relationship** (Followers you known). After this dictionary is returned, UtilMeta will complete the subsequent aggregate query and result distribution. Finally, the followers_you_known field of each user Schema instance will contain the query results that meet the condition requirement
478454

479-
!!! tip "Dynamic Schema Query"
480-
For the above example, you can call `UserSchema.get_runtime_schema(request_user_id)` in the API function to get the dynamic generated Schema class based on the user id of the current request, we often call it **Dynamic Schema Query**
481-
482-
455+
!!! tip
456+
Query function can use properties of `request.var` as the request context variables, such as:
457+
`request.var.user_id`:Current request user ID,
458+
`request.var.user`: Current request user instance,
459+
`request.var.ip`: Current request IP address.
460+
461+
You can pass the current request object (`self.request` of API) as the `context` param of the `init` / `serialize` method, such as `UserSchema.init(1, context=self.request)` to query the context fields
462+
483463
### Value query function
484464

485465
Above, we introduced the relational query function, which needs to correspond the primary key of the currently queried data with the primary key of the related model for the target field, and then serialize it using the relational model schema defined by the field.
@@ -555,45 +535,45 @@ Here are some expressions that are commonly used in real-world development
555535
#### `Exists`
556536
Sometimes you need to return the field of whether a conditional queryset exists, for example, when querying a user, you can use `Exists` an expression to return "**whether the current request user has followed**".
557537

558-
```python hl_lines="11"
538+
```python hl_lines="9"
559539
from utilmeta.core import orm
560540
from django.db import models
561541

562542
class UserSchema(orm.Schema[User]):
563543
username: str
564-
following: bool = False
565544

566545
@classmethod
567-
def get_runtime(cls, user_id):
568-
class user_schema(cls):
569-
following: bool = models.Exists(
570-
Follow.objects.filter(
571-
following=models.OuterRef('pk'),
572-
follower=user_id
573-
)
574-
)
575-
return user_schema
546+
def get_followed(cls, user_id: int = request.var.user_id):
547+
return models.Exists(
548+
User.objects.filter(
549+
pk=user_id,
550+
followers=exp.OuterRef('pk')
551+
)
552+
)
553+
554+
followed: bool = orm.Field(get_followed, default=False)
576555
```
577556
#### `SubqueryCount`
578557
For some relation counts you may need to add some conditions, for example, when querying an article, you need to return "**how many of the current user’s followings liked the article**", in which case you can use `SubqueryCount` expressions.
579558

580559
```python hl_lines="10"
560+
from utilmeta.core import orm, request
581561
from utilmeta.core.orm.backends.django import expressions as exp
582562

583563
class ArticleSchema(orm.Schema[Article]):
584564
id: int
585565
content: str
586566

587567
@classmethod
588-
def get_runtime_schema(cls, user_id):
589-
class article_schema(cls):
590-
following_likes: int = exp.SubqueryCount(
591-
User.objects.filter(
592-
followers=user_id,
593-
favorites=exp.OuterRef('pk')
594-
)
568+
def get_following_likes(cls, user_id: int = request.var.user_id):
569+
return exp.SubqueryCount(
570+
User.objects.filter(
571+
followers=user_id,
572+
likes=exp.OuterRef('pk')
595573
)
596-
return article_schema
574+
)
575+
576+
following_likes: int = orm.Field(get_following_likes, default_factory=list)
597577
```
598578

599579
!!! tip
@@ -794,6 +774,41 @@ In addition, you can specify a query expression with the `query` parameter of `
794774
* `default`: Specify default values for query parameter
795775
* `alias`: Specify an alias for the query parameter
796776

777+
#### Search Param
778+
779+
A commonly used type of query parameter is used to search for fuzzy matching of one or more fields of the target. Such search parameter can be defined using the `orm.Search` in UtilMeta, such as
780+
781+
```python
782+
from utilmeta.core import orm
783+
from datetime import datetime
784+
from django.db import models
785+
786+
class ArticleQuery(orm.Query[Article]):
787+
search: str = orm.Search(
788+
Article.title,
789+
Article.content,
790+
Article.slug,
791+
Article.tags
792+
)
793+
794+
class ArticleAPI(api.API):
795+
def get(self, query: ArticleQuery) -> List[ArticleSchema]:
796+
return ArticleSchema.serialize(query)
797+
```
798+
799+
`orm.Search` can receive strings or a field references of model that can be used for searching (usually text strings containing the main information of the model, such as title, summary, description, etc.), in addition, there are some search settings that can be specified:
800+
801+
* `case_sensitive`: Whether to search case sensitively, default to False
802+
* `keyword_delimiters`: Specify the delimiter for keywords. the search text will be divided into keywords first, and then the fields will be queried and matched. The default is `(' ', ',', ';', '|')`
803+
* `match_mode`: Match mode for field and keywords, includes:
804+
* `orm.Search.ANY_ANY`: Match any field with any keyword for the most relaxed search
805+
* `orm.Search.UNION_ALL`: The searchable fields of the instance can match all keywords (which can be divided into any one or more fields)
806+
* `orm.Search.ANY_ALL`: Any single searchable field needs to match all keywords, **default**
807+
* `orm.Search.ALL_ALL`: All fields need to match all keywords, the strictest search
808+
809+
!!! note
810+
UtilMeta >= 2.8.0 支持 `orm.Search` 搜索参数
811+
797812
### Sorting params
798813

799814
You can also declare sorting parameter in `orm.Query` class. with the supported sorting fields and the corresponding configuration. Examples are as follows

docs/zh/community/release.md

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

33
## v2.8
44

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

79
* API 接口支持 Server-Sent Events (SSE) 事件流响应: [API 中实现 SSE](../../guide/api-route#server-sent-events)
810
* Client 客户端支持处理 Server-Sent Events (SSE) 等流式响应: [处理 SSE 响应](../../guide/client#server-sent-events)
911
* `@api` 装饰器新增 `timeout` 参数,用于为 API 类的处理函数指定超时时间或为 Client 类的请求函数指定请求超时
1012
* 新增 HTTP 缓存插件 `api.Cache`:可以自动处理 Last-Modifed / Etag 等 HTTP 缓存机制
13+
* 新增搜索参数 `orm.Search`, 可以支持多种搜索模式对多个模型字段进行搜索匹配
14+
* `orm.Schema` 支持在查询函数中定义请求上下文参数(如当前请求用户 / IP),在序列化时通过 `context` 参数传当前请求对象实现请求上下文字段的查询
15+
* `meta gen_openapi` 命令支持 `--split` 参数,可以将 API 文档按照接口输出到独立的文件中,每个接口文档文件都包含接口的完整参数与响应定义,更方便 AI 工具解析读取
1116

1217
### 优化项
1318

docs/zh/community/roadmap.md

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

33
## 长期功能规划
44

5-
### WebSocket / SSE
5+
### WebSocket
66

7-
* 支持声明式语法编写 WebSocket / SSE / Server Push 接口
8-
* 支持 WebSocket / SSE 接口的文档生成与调试
7+
* 支持声明式语法编写 WebSocket 接口
8+
* 支持 WebSocket 接口的文档生成与调试
99

1010
### GraphQL
1111

@@ -31,7 +31,7 @@
3131

3232
## 后续版本规划
3333

34-
### v2.8
34+
### v2.9
3535

3636
**新特性**
3737

docs/zh/guide/api-route.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ class RootAPI(api.API):
650650

651651
此外,利用响应处理钩子,你还可以批量为接口生成响应,响应处理钩子的返回结果会代替 API 函数的返回结果作为响应进行返回,比如
652652
```python hl_lines="28-29"
653-
from utilmeta.core import api, orm, request
653+
from utilmeta.core import api, orm, request, response
654654

655655
class SingleArticleResponse(response.Response):
656656
result_key = 'article'
@@ -743,7 +743,7 @@ class RootAPI(api.API):
743743
def handle_user_auth(self, e: Error):
744744
return self.response(state=State.AUTH_FAILED, error=e)
745745

746-
@api.handle('*', exc.Notfound)
746+
@api.handle('*', exc.NotFound)
747747
def handle_not_found(self, e: Error):
748748
return self.response(state=State.NOT_FOUND, error=e)
749749
```

0 commit comments

Comments
 (0)