一种简单的api查询语言
apiql的Python项目详细描述
apiql-一种用于restful服务的简单api查询语言
apiql是一种简单的域语言,供api使用者以简洁、强大、url友好的方式表达rest资源过滤条件。
安装
可以使用pip安装和更新apiql:
pip安装apiql
API查询语言
api查询语言提供了一组谓词和表达式,允许api提供者和使用者构建复杂的资源查询。虽然apiql语法在很大程度上受sql启发,但它的设计与技术无关。apiql被转换成抽象的标准树,并且可以转换成后端特定的功能。
apiql查询语法
apiql查询是一组基本谓词,然后可以使用连接(和
)或析取(或
)组合成表达式。
例如,假设我们有rest api公开电影信息:
wget-q-o-'http://awso.me/api/movies'
[{"title":"Monty Python and the Holy Grail","release_year":1975,"original_title":"Monty Python and the Holy Grail""created_datetime":"2019-05-01T09:17:06.527181+00:00","external_id":"762","genres":[{"id":1,"name":"Comedy"},{"id":2,"name":"Adventure"},{"id":2,"name":"Fantasy"}],"id":1,"plot":"King Arthur, accompanied by his squire, recruits his Knights of the Round Table [...]","rating":6.0,"source":"tmdb","ignored":false},...]
apiql查询基础知识
在最基本的形式中,apiql查询只是一个谓词:
wget-q-o-'http://awso.me/api/movies?filter=title=="巨蟒与圣杯"'
它只过滤"monty python and the holy grails"资源。(整个apiql查询包含在单个url查询参数中;请注意,apiql使用=
运算符实现相等,而不是=
)。
注意:apiql被设计成尽可能友好的url,但是所有apiql查询都应该是url编码的。在本文档中,我们使用wget
因为它默认情况下url编码所有url;curl
在本例中有一点更神秘的语法:curl-g--data urlencode"filter=[my apiql query]"http://awso.me/api/movies
apiql谓词
实际上,api使用者需要的不仅仅是简单的谓词。APIQL支持以下谓词集:
=
, < L> >代码>!=,>;
,>;=
,<;
,<;=
,like
-等效的类sql运算符,但是不需要显式添加%
,我喜欢
-类似于, 的不区分大小写版本
not like
-等效的sql notlike运算符,notilike
-不区分大小写的版本starts with
-相当于sql starts with operator,istartswith
-不区分大小写版本的startswith
,ends with
-相当于sql以, 结尾
IEndswith
-不区分大小写版本的IEndswith
,包含
-alias tolike
,notcontains
-别名为notlike
,icontains
-不区分大小写版本的包含
,inotcontains
-不区分大小写的版本notcontains
,输入
-相当于SQL输入运算符,not in
相当于sql not in运算符。
例如,查询:
wget-q-o-'http://awso.me/api/movies?filter=title我喜欢"神圣"'
将返回与"holy"匹配的所有电影:"monty python and the holy grails","holy money"和可能与"holy"匹配的一堆其他电影。
以及查询:
wget-q-o-'http://awso.me/api/movies?过滤器=发布年份>;=1975'
将返回1975年或以后发行的所有电影。
查询文本
文字是值,可以在谓词的右边。到目前为止,我们已经看到字符串("神圣")和数字文字(1975年)。apiql也支持许多其他文本:
- 字符串-所有字符串文本都是Unicode的,并且遵循与JSON字符串文本相同的规则s.apiql字符串总是用双引号括起来(例如,这是一个字符串:"这是一个字符串",但是:"不是字符串"不是),并转义("电影:\"电影")。
- 数字-基本上是整数和浮点数:
发布年份!=2003
或评级>;3.3
甚至评级>;-1.6e-35
- 布尔值-
true
和false
被转换为特定于平台的布尔值。示例用法:ingored!=假
- nil-special
null
literal被转换为特定于平台的literal,例如:
genres!=空
- datetime-表示日期时间的文字:
created_datetime>;=日期时间("2019-05-01t08:00:00.527181+00:00")
。开箱即用apiql支持iso-8601日期时间格式(但是,可以自定义此行为)。 - 元组-表示
in
和notin
子句中的一系列值:release\u year notin(1975,2011)
。元组可以包含其他文本的coma分隔列表:发布日期in(flase,null,datetime("datetime("1975-01-01t00:00:00.000000+00:00"))
组合查询
通过对原子谓词进行分组(用;
分隔),可以将查询组合成更复杂的表达式。
例如:
wget-q-o-'http://awso.me/api/movies?filter=title ilike"holy";release_year>;1975;忽略!=空'
此查询中的谓词被解释为连词
(和
),返回1975年以后发行的所有与标题
匹配的"holy"和电影,这些电影未标记为忽略
逻辑表达式
apiql支持逻辑连接(和
)和析取
(或
);它们都可以嵌套谓词:和(criteria(;criteria)*)
和或(criteria(;criteria)*)
下面的查询,相当于上一个查询:
wget-q-o-'http://awso.me/api/movies?filter=和(标题Ilike"Holy";发布年份>;1975;忽略!=真)
不过,这一个:
wget-q-o-'http://awso.me/api/movies?filter=或(标题Ilike"Holy";发布年份>;1975;忽略!=真)
将返回所有标题与1975年后发行的"神圣的"或匹配且未被忽略的电影。
连接和析取可以嵌套。假设我们希望筛选分级大于7或源为"imdb"的电影,但是我们只希望筛选未被忽略的资源:
wget-q-o-'http://awso.me/api/movies?filter=和(或(rating>;7;source="imdb");忽略!=flase)
分析APIQL查询
到目前为止,还不错。现在,api数据存储如何解释apiql查询呢?apiql查询在内部转换为python数据结构(语法树),由crtieria
类表示。
条件
以及谓词
、连接
和分离
完全表示已解析的查询树。
条件
类聚合条件的列表
条件
只是抽象的原子条件元素;它要么是:
- 简单的谓词原子逻辑表达式(例如
谓词('title','==','apocalypse now')
用于查询title=="apocalypse now"
) 连接
这也是一个逻辑和
运算符,它带有一组谓词连接([predicate('title','==','apocalypse now'),predicate('release_year','>;',1975])
用于查询和(title=="apocalype now";release_year>;1975)
析取
-逻辑或
运算符
用python解析apiql查询:
importapiql.parserasparserfromapiql.criteriaimportCriteria,Conjunction,Disjunction,Predicate# ...parsed_criteria=parser.parse('and(title like "Monty";genres == null;ignored!=false;release_year<=1975)')syntax_tree=Criteria([Conjunction([Predicate('title','like','Monty'),Predicate('genres','==',None),Predicate('ignored','!=',False),Predicate('release_year','<=',1975)])])# parsed_criteria is equal to syntax_treeassertEqual(syntax_tree,parsed_criteria)
查询之旅
现在我们可以使用apiql来过滤资源。由于box apiql提供sqlalchemy orm集成(但是可以与其他后端集成)。本节展示了所有基本查询示例。完整的查询功能列表可以在测试套件中找到。
所有示例都将使用此示例数据模型:
Base=declarative_base()classGenre(Base):__tablename__='genre'id=Column(Integer,primary_key=True)name=Column(String)genre_id=Column(Integer)movie_id=Column(Integer,ForeignKey('movie.id'),nullable=True)classMovie(Base):__tablename__='movie'id=Column(Integer,primary_key=True)title=Column(String)original_title=Column(String)release_year=Column(Integer)source=Column(String)rating=Column(String)created_datetime=Column(DateTime,default=datetime.utcnow)genres=relationship('Genre',cascade="all",backref="movie",lazy=True)drama=Genre(name="Drama",genre_id=1)scifi=Genre(name="Sci-Fi",genre_id=2)war=Genre(name="War",genre_id=3)adventure=Genre(name="Adventure",genre_id=4)comedy=Genre(name="Comedy",genre_id=5)monty_python=Movie(title="Monty Python and the Holy Grail",release_year=1975,source="IMDB",rating="8",genres=[comedy,adventure])jurassic_park=Movie(title="Jurassic Park",release_year=1993,source='IMDB',rating="9",genres=[adventure,scifi])apocalypse_now=Movie(title="Apocalypse Now",release_year=1979,source="TMDB",rating="9",original_title="Apocalypse Now, The",genres=[drama,war])gosford_park=Movie(title="Gosford Park",release_year=2001,source='IMDB',rating="7",genres=[drama])session.add(monty_python)session.add(jurassic_park)session.add(apocalypse_now)session.add(gosford_park)
sqlalchemy集成的一个主要入口点是with_criteria
扩展方法,它使用apiql功能扩展普通的sqlalchemy查询
对象。with_criteria
遵循基本的sqlalchemy约定,因此它可以与nativefilter_by
或filter
函数一起自由使用。
以下示例显示APIQL查询及其本机SQLalchemy表示。
简单连接条件
fromapiql.backends.sqlalchemy.ormimportwith_criteriaactual=session.query(Movie).with_criteria('and(rating=="8";release_year==1975)')# is equivalent toexpected=session.query(Movie).filter(and_(Movie.rating==8,Movie.release_year==1975))
简单析取准则
fromapiql.backends.sqlalchemy.ormimportwith_criteriaactual=session.query(Movie).with_criteria('or(rating=="8";release_year==1993;source=="TMDB")')# is equivalent toexpected=session.query(Movie).filter(or_(Movie.rating==8,Movie.release_year==1993,Movie.source=='TMDB'))
<;
和>;
谓词
actual=session.query(Movie).with_criteria('and(release_year > 1975; release_year < 2001)')# is equivalent toexpected=session.query(Movie).filter(and_(Movie.release_year>1975,Movie.release_year<2001))
类
和类
谓词
actual=session.query(Movie).with_criteria('or(title like "THE"; original_title ilike "THE")')# is equivalent toexpected=session.query(Movie).filter(or_(Movie.title.like('%THE%'),Movie.original_title.ilike("%THE%")))
不喜欢
谓词
actual=session.query(Movie).with_criteria('and(title notlike "the"; release_year > 1990)')# is equivalent toexpected=session.query(Movie).filter(and_(Movie.title.notlike('%the%'),Movie.release_year>1990))
in
和notin
谓词
actual=session.query(Movie).with_criteria('release_year in (1979, 2001))')# is equivalent toexpected=session.query(Movie).filter(Movie.release_year.in_((1979,2001)))
fromapiql.backends.sqlalchemy.ormimportwith_criteriaactual=session.query(Movie).with_criteria('or(rating=="8";release_year==1993;source=="TMDB")')# is equivalent toexpected=session.query(Movie).filter(or_(Movie.rating==8,Movie.release_year==1993,Movie.source=='TMDB'))
<;
和>;
谓词
actual=session.query(Movie).with_criteria('and(release_year > 1975; release_year < 2001)')# is equivalent toexpected=session.query(Movie).filter(and_(Movie.release_year>1975,Movie.release_year<2001))
类
和类
谓词
actual=session.query(Movie).with_criteria('or(title like "THE"; original_title ilike "THE")')# is equivalent toexpected=session.query(Movie).filter(or_(Movie.title.like('%THE%'),Movie.original_title.ilike("%THE%")))
不喜欢
谓词
actual=session.query(Movie).with_criteria('and(title notlike "the"; release_year > 1990)')# is equivalent toexpected=session.query(Movie).filter(and_(Movie.title.notlike('%the%'),Movie.release_year>1990))
in
和notin
谓词
actual=session.query(Movie).with_criteria('release_year in (1979, 2001))')# is equivalent toexpected=session.query(Movie).filter(Movie.release_year.in_((1979,2001)))
类
和类
谓词不喜欢
谓词in
和notin
谓词
actual=session.query(Movie).with_criteria('release_year in (1979, 2001))')# is equivalent toexpected=session.query(Movie).filter(Movie.release_year.in_((1979,2001)))
和
[{"title":"Monty Python and the Holy Grail","release_year":1975,"original_title":"Monty Python and the Holy Grail""created_datetime":"2019-05-01T09:17:06.527181+00:00","external_id":"762","genres":[{"id":1,"name":"Comedy"},{"id":2,"name":"Adventure"},{"id":2,"name":"Fantasy"}],"id":1,"plot":"King Arthur, accompanied by his squire, recruits his Knights of the Round Table [...]","rating":6.0,"source":"tmdb","ignored":false},...]0
为空
且不为空
谓词
[{"title":"Monty Python and the Holy Grail","release_year":1975,"original_title":"Monty Python and the Holy Grail""created_datetime":"2019-05-01T09:17:06.527181+00:00","external_id":"762","genres":[{"id":1,"name":"Comedy"},{"id":2,"name":"Adventure"},{"id":2,"name":"Fantasy"}],"id":1,"plot":"King Arthur, accompanied by his squire, recruits his Knights of the Round Table [...]","rating":6.0,"source":"tmdb","ignored":false},...]1
和
[{"title":"Monty Python and the Holy Grail","release_year":1975,"original_title":"Monty Python and the Holy Grail""created_datetime":"2019-05-01T09:17:06.527181+00:00","external_id":"762","genres":[{"id":1,"name":"Comedy"},{"id":2,"name":"Adventure"},{"id":2,"name":"Fantasy"}],"id":1,"plot":"King Arthur, accompanied by his squire, recruits his Knights of the Round Table [...]","rating":6.0,"source":"tmdb","ignored":false},...]2
使用
和结束谓词启动
以下查询是等价的
[{"title":"Monty Python and the Holy Grail","release_year":1975,"original_title":"Monty Python and the Holy Grail""created_datetime":"2019-05-01T09:17:06.527181+00:00","external_id":"762","genres":[{"id":1,"name":"Comedy"},{"id":2,"name":"Adventure"},{"id":2,"name":"Fantasy"}],"id":1,"plot":"King Arthur, accompanied by his squire, recruits his Knights of the Round Table [...]","rating":6.0,"source":"tmdb","ignored":false},...]3
和
[{"title":"Monty Python and the Holy Grail","release_year":1975,"original_title":"Monty Python and the Holy Grail""created_datetime":"2019-05-01T09:17:06.527181+00:00","external_id":"762","genres":[{"id":1,"name":"Comedy"},{"id":2,"name":"Adventure"},{"id":2,"name":"Fantasy"}],"id":1,"plot":"King Arthur, accompanied by his squire, recruits his Knights of the Round Table [...]","rating":6.0,"source":"tmdb","ignored":false},...]4
包含
谓词
[{"title":"Monty Python and the Holy Grail","release_year":1975,"original_title":"Monty Python and the Holy Grail""created_datetime":"2019-05-01T09:17:06.527181+00:00","external_id":"762","genres":[{"id":1,"name":"Comedy"},{"id":2,"name":"Adventure"},{"id":2,"name":"Fantasy"}],"id":1,"plot":"King Arthur, accompanied by his squire, recruits his Knights of the Round Table [...]","rating":6.0,"source":"tmdb","ignored":false},...]5
日期时间
文本
以下查询是等价的
[{"title":"Monty Python and the Holy Grail","release_year":1975,"original_title":"Monty Python and the Holy Grail""created_datetime":"2019-05-01T09:17:06.527181+00:00","external_id":"762","genres":[{"id":1,"name":"Comedy"},{"id":2,"name":"Adventure"},{"id":2,"name":"Fantasy"}],"id":1,"plot":"King Arthur, accompanied by his squire, recruits his Knights of the Round Table [...]","rating":6.0,"source":"tmdb","ignored":false},...]6
连接和别名实体
连接也受支持。以下查询是等价的:
[{"title":"Monty Python and the Holy Grail","release_year":1975,"original_title":"Monty Python and the Holy Grail""created_datetime":"2019-05-01T09:17:06.527181+00:00","external_id":"762","genres":[{"id":1,"name":"Comedy"},{"id":2,"name":"Adventure"},{"id":2,"name":"Fantasy"}],"id":1,"plot":"King Arthur, accompanied by his squire, recruits his Knights of the Round Table [...]","rating":6.0,"source":"tmdb","ignored":false},...]7
apiql支持别名
实体:
[{"title":"Monty Python and the Holy Grail","release_year":1975,"original_title":"Monty Python and the Holy Grail""created_datetime":"2019-05-01T09:17:06.527181+00:00","external_id":"762","genres":[{"id":1,"name":"Comedy"},{"id":2,"name":"Adventure"},{"id":2,"name":"Fantasy"}],"id":1,"plot":"King Arthur, accompanied by his squire, recruits his Knights of the Round Table [...]","rating":6.0,"source":"tmdb","ignored":false},...]8
使用烧瓶sqlalchemy完成API示例
apiql在任何可能的情况下都与技术无关,可以与所有流行的python web框架(flask、bottle、django等)一起使用。
(附带说明:apiql引用实现也完全支持flask sqlalchemy扩展)。
这是一个简单的flask api,带有apiql过滤(假设movie
和genre
类是json
serializable)
[{"title":"Monty Python and the Holy Grail","release_year":1975,"original_title":"Monty Python and the Holy Grail""created_datetime":"2019-05-01T09:17:06.527181+00:00","external_id":"762","genres":[{"id":1,"name":"Comedy"},{"id":2,"name":"Adventure"},{"id":2,"name":"Fantasy"}],"id":1,"plot":"King Arthur, accompanied by his squire, recruits his Knights of the Round Table [...]","rating":6.0,"source":"tmdb","ignored":false},...]9
现在我们可以使用apiql筛选资源:
wget-q-o-'localhost:5000/api/电影?filter=和(或(标题类似"python";原始标题类似"python");source=="tmdb")'
注意:当API使用者未指定筛选查询时,我们可以使用空字符串。
此查询:
wget-q-o-"本地主机:5000/api/movies"
将返回所有未筛选的资源。
白名单api属性
在大多数情况下,api提供者只提供对消费者可以使用的属性的有限访问。apiql通过在查询中访问哪些属性来支持此功能(无论资源中公开了哪些属性,这都不会改变)。
可以使用白名单
查询
扩展方法启用白名单。默认情况下,所有资源属性都是白名单
仅白名单特定属性
apiqljust
builder只会白名单显式指定的资源属性。
importapiql.parserasparserfromapiql.criteriaimportCriteria,Conjunction,Disjunction,Predicate# ...parsed_criteria=parser.parse('and(title like "Monty";genres == null;ignored!=false;release_year<=1975)')syntax_tree=Criteria([Conjunction([Predicate('title','like','Monty'),Predicate('genres','==',None),Predicate('ignored','!=',False),Predicate('release_year','<=',1975)])])# parsed_criteria is equal to syntax_treeassertEqual(syntax_tree,parsed_criteria)0
现在,这个查询:
wget-q-o-'http://localhost:5000/api/movies?过滤器=发布年份==2001'
将工作正常,因为movie.release_year已白名单,
但是,此呼叫:
wget-q-o-'http://localhost:5000/api/movies?过滤器=额定值="8.0"'
将失败:
valueerror:无效的查询属性:"rating"
白名单所有属性
所有内容
生成器白名单所有实体(或实体)属性。这是未指定白名单时的默认行为。apiql引擎将扫描所有查询
实体,并列出所有属性。
importapiql.parserasparserfromapiql.criteriaimportCriteria,Conjunction,Disjunction,Predicate# ...parsed_criteria=parser.parse('and(title like "Monty";genres == null;ignored!=false;release_year<=1975)')syntax_tree=Criteria([Conjunction([Predicate('title','like','Monty'),Predicate('genres','==',None),Predicate('ignored','!=',False),Predicate('release_year','<=',1975)])])# parsed_criteria is equal to syntax_treeassertEqual(syntax_tree,parsed_criteria)1
注意,在这种情况下,只有电影
属性被白名单,而所有流派
属性都不被白名单。
在这种情况下:
wget-q-o-'http://localhost:5000/API/电影?过滤器=额定值="8.0"'
将按预期工作。但是,此查询:
wget-q-o-'http://localhost:5000/api/movies?filter=name=="科幻"'
将再次失败。
白名单所有属性,指定的属性集除外
除
之外的所有内容生成器都会列出所有属性,但在but
子句中指定的属性除外。
importapiql.parserasparserfromapiql.criteriaimportCriteria,Conjunction,Disjunction,Predicate# ...parsed_criteria=parser.parse('and(title like "Monty";genres == null;ignored!=false;release_year<=1975)')syntax_tree=Criteria([Conjunction([Predicate('title','like','Monty'),Predicate('genres','==',None),Predicate('ignored','!=',False),Predicate('release_year','<=',1975)])])# parsed_criteria is equal to syntax_treeassertEqual(syntax_tree,parsed_criteria)2
现在,此调用将失败,因为movie.id
没有白名单:
wget-q-o-'http://localhost:5000/api/movies?过滤器=id==1'
合并白名单
最后,合并的
白名单生成器可以合并(合并)两个白名单。以下示例:
importapiql.parserasparserfromapiql.criteriaimportCriteria,Conjunction,Disjunction,Predicate# ...parsed_criteria=parser.parse('and(title like "Monty";genres == null;ignored!=false;release_year<=1975)')syntax_tree=Criteria([Conjunction([Predicate('title','like','Monty'),Predicate('genres','==',None),Predicate('ignored','!=',False),Predicate('release_year','<=',1975)])])# parsed_criteria is equal to syntax_treeassertEqual(syntax_tree,parsed_criteria)3
将列出movie
中的所有属性,并且只列出genre.name
fromgenre
前缀属性
可以在查询属性前面加上前缀,以便更方便用户使用。例如,在上面的示例中,name
属性将解析为genre.name
(这里movie
和genre
之间没有列名冲突)。然而,从消费者的角度来看,将这个属性映射到更明显的东西会更优雅;前缀正是这样做的。下面是一个示例:
importapiql.parserasparserfromapiql.criteriaimportCriteria,Conjunction,Disjunction,Predicate# ...parsed_criteria=parser.parse('and(title like "Monty";genres == null;ignored!=false;release_year<=1975)')syntax_tree=Criteria([Conjunction([Predicate('title','like','Monty'),Predicate('genres','==',None),Predicate('ignored','!=',False),Predicate('release_year','<=',1975)])])# parsed_criteria is equal to syntax_treeassertEqual(syntax_tree,parsed_criteria)4
映射属性
有时我们希望以不同的名称公开查询属性(例如,我们希望保持向后契约的兼容性)。映射的
函数is就是这样做的。
假设我们想映射genre.name
来查询属性kind
,这样我们就可以使用更好的查询,比如kind="war"
importapiql.parserasparserfromapiql.criteriaimportCriteria,Conjunction,Disjunction,Predicate# ...parsed_criteria=parser.parse('and(title like "Monty";genres == null;ignored!=false;release_year<=1975)')syntax_tree=Criteria([Conjunction([Predicate('title','like','Monty'),Predicate('genres','==',None),Predicate('ignored','!=',False),Predicate('release_year','<=',1975)])])# parsed_criteria is equal to syntax_treeassertEqual(syntax_tree,parsed_criteria)5