Python HAL生成/解析库
项目描述
卤素
Python HAL 生成/解析库。
卤素利用声明式样式的序列化,具有易于扩展的模式。模式结合了关于您数据模型、属性映射和高级访问的知识,以及复杂类型和数据转换。
库旨在以最明显的方式表示您的数据为 HAL 格式,同时也提供了通用的类似网页表单的功能,以便尽可能多地重用您的模式和类型。
模式
模式是序列化的主要构建块。它也是一种类型,这意味着您可以使用模式声明嵌套结构。
序列化
>>> Schema.serialize({"hello": "Hello World"})
>>> {"hello": "Hello World"}
简单地调用 Schema.serialize() 类方法,它可以接受字典或任何其他对象。
验证
序列化过程中不涉及验证。您的源数据或模型被认为是干净的,因为它来自存储,并且不是用户输入。当然,类型或属性访问器可能发生异常,但它们被认为是编程错误。
序列化字典
字典值会自动通过模式属性使用它们的名称作为键进行访问
import halogen
class Hello(halogen.Schema):
hello = halogen.Attr()
serialized = Hello.serialize({"hello": "Hello World"})
结果
{
"hello": "Hello World"
}
HAL 只是 JSON,但根据其规范,它应该有自链接来标识序列化的资源。为此,您应使用 HAL 特定属性并配置 self 的组合方式。
HAL 示例
import halogen
from flask import url_for
spell = {
"uid": "abracadabra",
"name": "Abra Cadabra",
"cost": 10,
}
class Spell(halogen.Schema):
self = halogen.Link(attr=lambda spell: url_for("spell.get" uid=spell['uid']))
name = halogen.Attr()
serialized = Spell.serialize(spell)
结果
{
"_links": {
"self": {"href": "/spells/abracadabra"}
},
"name": "Abra Cadabra"
}
序列化对象
与字典键类似,模式属性也可以访问对象属性
import halogen
from flask import url_for
class Spell(object):
uid = "abracadabra"
name = "Abra Cadabra"
cost = 10
spell = Spell()
class SpellSchema(halogen.Schema):
self = halogen.Link(attr=lambda spell: url_for("spell.get" uid=spell.uid))
name = halogen.Attr()
serialized = SpellSchema.serialize(spell)
结果
{
"_links": {
"self": {"href": "/spells/abracadabra"}
},
"name": "Abra Cadabra"
}
属性
属性构成模式,并封装了从您的模型中获取数据、根据特定类型转换数据的知识。
Attr()
模式中属性成员的名称是结果序列化到的键的名称。默认情况下,使用相同的属性名称访问源模型。
示例
import halogen
from flask import url_for
class Spell(object):
uid = "abracadabra"
name = "Abra Cadabra"
cost = 10
spell = Spell()
class SpellSchema(halogen.Schema):
self = halogen.Link(attr=lambda spell: url_for("spell.get" uid=spell.uid))
name = halogen.Attr()
serialized = SpellSchema.serialize(spell)
结果
{
"_links": {
"self": {"href": "/spells/abracadabra"}
},
"name": "Abra Cadabra"
}
Attr("const")
如果属性表示一个常量,则可以将值指定为第一个参数。这个第一个参数是属性的类型的实例或子类。如果类型不是 halogen.types.Type 的实例或子类,则会被忽略。
import halogen
from flask import url_for
class Spell(object):
uid = "abracadabra"
name = "Abra Cadabra"
cost = 10
spell = Spell()
class SpellSchema(halogen.Schema):
self = halogen.Link(attr=lambda spell: url_for("spell.get" uid=spell.uid))
name = halogen.Attr("custom name")
serialized = SpellSchema.serialize(spell)
结果
{
"_links": {
"self": {"href": "/spells/abracadabra"}
},
"name": "custom name"
}
在某些情况下,也可以将 attr 指定为可调用的函数,它返回一个常量值。
Attr(attr="foo")
如果属性名称与您的模型不匹配,则可以覆盖它
import halogen
from flask import url_for
class Spell(object):
uid = "abracadabra"
title = "Abra Cadabra"
cost = 10
spell = Spell()
class SpellSchema(halogen.Schema):
self = halogen.Link(attr=lambda spell: url_for("spell.get" uid=spell.uid))
name = halogen.Attr(attr="title")
serialized = SpellSchema.serialize(spell)
结果
{
"_links": {
"self": {"href": "/spells/abracadabra"}
},
"name": "Abra Cadabra"
}
attr 参数接受源属性名称的字符串或点分隔的属性路径。这对于嵌套字典、相关对象和 Python 属性都有效。
import halogen
class SpellSchema(halogen.Schema):
name = halogen.Attr(attr="path.to.my.attribute")
Attr(attr=lambda value: value)
attr 参数接受可调用的函数,该函数接受整个源模型并可以访问必要的属性。您可以通过传递函数或 lambda 来返回所需值,该值也可以只是一个常量。
import halogen
from flask import url_for
class Spell(object):
uid = "abracadabra"
title = "Abra Cadabra"
cost = 10
spell = Spell()
class SpellSchema(halogen.Schema):
self = halogen.Link(attr=lambda spell: url_for("spell.get" uid=spell.uid))
name = halogen.Attr(attr=lambda value: value.title)
serialized = SpellSchema.serialize(spell)
结果
{
"_links": {
"self": {"href": "/spells/abracadabra"}
},
"name": "Abra Cadabra"
}
属性作为装饰器
有时访问器函数太大,不能使用 lambda。在这种情况下,可以将类的方法装饰为获取器访问器。
import halogen
class ShoppingCartSchema(halogen.Schema):
@halogen.attr(AmountType(), default=None)
def total(obj):
return sum(
(item.amount for item in obj.items),
0,
)
@total.setter
def set_total(obj, value):
obj.total = value
Attr(attr=Acccessor)
如果模式用于双向序列化和反序列化,则可以传递 halogen.schema.Accessor,其中指定了 getter 和 setter。 Getter 是一个字符串或可调用的函数,用于从模型中获取值,而 setter 是一个字符串或可调用的函数,它知道反序列化的值应该存储在哪里。
Attr(Type())
属性获取值后,将其传递给其类型以完成序列化。卤素提供基本类型,例如 halogen.types.List 以实现值或模式的列表。模式也是一种类型,可以传递给属性以实现复杂结构。
示例
import halogen
from flask import url_for
class Book(object):
uid = "good-book-uid"
title = "Harry Potter and the Philosopher's Stone"
genres = [
{"uid": "fantasy-literature", "title": "fantasy literature"},
{"uid": "mystery", "title": "mystery"},
{"uid": "adventure", "title": "adventure"},
]
book = Book()
class GenreSchema(halogen.Schema):
self = halogen.Link(attr=lambda genre: url_for("genre.get" uid=genre['uid']))
title = halogen.Attr()
class BookSchema(halogen.Schema):
self = halogen.Link(attr=lambda book: url_for("book.get" uid=book.uid))
title = halogen.Attr()
genres = halogen.Attr(halogen.types.List(GenreSchema))
serialized = BookSchema.serialize(book)
结果
{
"_links": {
"self": {"href": "good-book-uid"}
},
"genres": [
{"_links": {"self": {"href": "fantasy-literature"}}, "title": "fantasy literature"},
{"_links": {"self": {"href": "mystery"}}, "title": "mystery"},
{"_links": {"self": {"href": "adventure"}}, "title": "adventure"}
],
"title": "Harry Potter and the Philosopher's Stone"
}
Attr(Type(validators=[validator]))
类型可以获取可选的 validators 参数,它是一个包含 halogen.validators.Validator 对象的列表,其单个接口方法 validate 将在反序列化期间调用给定值。如果值无效,应引发 halogen.exceptions.ValidationError。卤素提供基本验证器,例如 halogen.validators.Range 以验证值是否在特定范围内。
Attr(default=value)
如果属性无法获取,将使用提供的 default 值;如果 default 值是可调用的,则将调用它以获取默认值。
Attr(required=False)
默认情况下,属性是必需的,因此在序列化期间无法获取属性且未提供默认值时,将引发异常(AttributeError 或 KeyError,具体取决于输入)。可以通过将 required=False 传递给属性构造函数来放宽此限制。对于反序列化,适用相同的逻辑,但异常类型将变为 halogen.exceptions.ValidationError 以便人类可读(参见 反序列化)。
Type
类型负责单个值的序列化,例如整数、字符串、日期。类型也是 Schema 的基础。它具有 serialize() 和 deserialize() 方法,可以将属性值进行转换。与 Schema 类型不同,类型是实例化的。您可以通过在声明您的 schema 时向其构造函数传递参数来配置序列化行为。
类型可以在反序列化期间引发 halogen.exceptions.ValidationError,但序列化期望该类型知道如何转换的值。
类型子类化
在您的应用程序中常见的类型可以在 schema 之间共享。这可能包括日期时间类型、特定 URL 类型、国际化字符串以及任何需要特定格式的其他表示。
Type.serialize
Type.serialize 的默认实现是一个绕过。
类型的序列化方法是序列化值时的最后一次转换机会
示例
import halogen
class Amount(object):
currency = "EUR"
amount = 1
class AmountType(halogen.types.Type):
def serialize(self, value):
if value is None or not isinstance(value, Amount):
return None
return {
"currency": value.currency,
"amount": value.amount
}
class Product(object):
name = "Milk"
def __init__(self):
self.price = Amount()
product = Product()
class ProductSchema(halogen.Schema):
name = halogen.Attr()
price = halogen.Attr(AmountType())
serialized = ProductSchema.serialize(product)
结果
{
"name": "Milk",
"price": {
"amount": 1,
"currency": "EUR"
}
}
可空类型
如果访问器返回 None 且不希望类型或嵌套 schema 进行进一步序列化,则可以将类型包装到 Nullable 类型中。
import halogen
class FreeProduct(object):
"""A free product, that doesn't have a price."""
price = None
class AmountSchema(halogen.Schema):
currency = halogen.Attr(required=True, default="USD")
amount = halogen.Attr(required=True, default=0)
class FreeProductSchema(halogen.Schema):
price_null = halogen.Attr(halogen.types.Nullable(AmountType()), attr="price")
price_zero = halogen.Attr(AmountType(), attr="price")
serialized = FreeProductSchema.serialize(FreeProduct())
结果
{
"price_null": None,
"price_zero": {
"amount": 0,
"currency": "USD"
}
}
HAL
超文本应用语言。
RFC
HAL(application/hal+json)的 JSON 变体已作为互联网草案发布: 草案-kelly-json-hal
链接
RFC 中的链接对象: 链接对象
href
“href”属性是必需的。
halogen.Link 将为您创建 href。您只需指向 halogen.Link 应该放在哪里或应该放入 href 中的内容即可。
- 静态变体
import halogen class EventSchema(halogen.Schema): artist = halogen.Link(attr="/artists/some-artist")
- 可调用变体
import halogen class EventSchema(halogen.Schema): help = halogen.Link(attr=lambda: current_app.config['DOC_URL'])
弃用
可以通过指定指向描述弃用的文档的弃用 URL 属性来弃用链接。
import halogen class EventSchema(halogen.Schema): artist = halogen.Link( attr="/artists/some-artist", deprecation="http://docs.api.com/deprecations#artist", )
CURIE
CURIE 提供了对资源文档的链接。
import halogen
doc = halogen.Curie(
name="doc,
href="http://haltalk.herokuapp.com/docs/{rel}",
templated=True
)
class BlogSchema(halogen.Schema):
lastest_post = halogen.Link(attr="/posts/latest", curie=doc)
{
"_links": {
"curies": [
{
"name": "doc",
"href": "http://haltalk.herokuapp.com/docs/{rel}",
"templated": true
}
],
"doc:latest_posts": {
"href": "/posts/latest"
}
}
}
Schema 也可以是链接的参数
import halogen
class BookLinkSchema(halogen.Schema):
href = halogen.Attr("/books")
class BookSchema(halogen.Schema):
books = halogen.Link(BookLinkSchema)
serialized = BookSchema.serialize({"books": ""})
{
"_links": {
"books": {
"href": "/books"
}
}
}
嵌入
保留的 “_embedded” 属性是可选的。它是一个对象,其属性名称是链接关系类型(如 [RFC5988] 中定义的)和值是资源对象或资源对象的数组。
嵌入资源可以是来自目标 URI 的表示的全版本、部分版本或不一致版本。
为了在您的模式中创建 _embedded,您应该使用 halogen.Embedded。
示例
import halogen
em = halogen.Curie(
name="em",
href="https://docs.event-manager.com/{rel}.html",
templated=True,
type="text/html"
)
class EventSchema(halogen.Schema):
self = halogen.Link("/events/activity-event")
collection = halogen.Link("/events/activity-event", curie=em)
uid = halogen.Attr()
class PublicationSchema(halogen.Schema):
self = halogen.Link(attr=lambda publication: "/campaigns/activity-campaign/events/activity-event")
event = halogen.Link(attr=lambda publication: "/events/activity-event", curie=em)
campaign = halogen.Link(attr=lambda publication: "/campaign/activity-event", curie=em)
class EventCollection(halogen.Schema):
self = halogen.Link("/events")
events = halogen.Embedded(halogen.types.List(EventSchema), attr=lambda collection: collection["events"], curie=em)
publications = halogen.Embedded(
attr_type=halogen.types.List(PublicationSchema),
attr=lambda collection: collection["publications"],
curie=em
)
collections = {
'events': [
{"uid": 'activity-event'}
],
'publications': [
{
"event": {"uid": "activity-event"},
"campaign": {"uid": "activity-campaign"}
}
]
}
serialized = EventCollection.serialize(collections)
结果
{
"_embedded": {
"em:events": [
{
"_links": {
"curies": [
{
"href": "https://docs.event-manager.com/{rel}.html",
"name": "em",
"templated": true,
"type": "text/html"
}
],
"em:collection": {"href": "/events/activity-event"},
"self": {"href": "/events/activity-event"}
},
"uid": "activity-event"
}
],
"em:publications": [
{
"_links": {
"curies": [
{
"href": "https://docs.event-manager.com/{rel}.html",
"name": "em",
"templated": true,
"type": "text/html"
}
],
"em:campaign": {"href": "/campaign/activity-event"},
"em:event": {"href": "/events/activity-event"},
"self": {"href": "/campaigns/activity-campaign/events/activity-event"}
}
}
]
},
"_links": {
"curies": [
{
"href": "https://docs.event-manager.com/{rel}.html",
"name": "em",
"templated": true,
"type": "text/html"
}
],
"self": {"href": "/events"}
}
}
默认情况下,嵌入式资源是必需的,您可以通过将 required=False 传递给构造函数来使它们不是必需的,并且在序列化中会省略空值。
import halogen
class Schema(halogen.Schema):
user1 = halogen.Embedded(PersonSchema, required=False)
user2 = halogen.Embedded(PersonSchema)
serialized = Schema.serialize({'user2': Person("John", "Smith")})
结果
{
"_embedded": {
"user2": {
"name": "John",
"surname": "Smith"
}
}
}
反序列化
模式具有 deserialize 方法。如果未传递任何对象作为第二个参数,则方法 deserialize 将返回反序列化结果的字典。
示例
import halogen
class Hello(halogen.Schema):
hello = halogen.Attr()
result = Hello.deserialize({"hello": "Hello World"})
print(result)
结果
{
"hello": "Hello World"
}
然而,如果您将对象作为 deserialize 方法的第二个参数传递,则数据将被分配到对象的属性中。
示例
import halogen
class HellMessage(object):
hello = ""
hello_message = HellMessage()
class Hello(halogen.Schema):
hello = halogen.Attr()
Hello.deserialize({"hello": "Hello World"}, hello_message)
print(hello_message.hello)
结果
"Hello World"
Type.deserialize
如您所知,属性从它们在序列化时支持的类型中调用 serialize 方法,但在反序列化时,相同的属性将调用 deserialize 方法。这意味着当您编写自己的类型时,您不应该忘记为它们编写 deserialize 方法。
示例
import halogen
import decimal
class Amount(object):
currency = "EUR"
amount = 1
def __init__(self, currency, amount):
self.currency = currency
self.amount = amount
def __repr__(self):
return "Amount: {currency} {amount}".format(currency=self.currency, amount=str(self.amount))
class AmountType(halogen.types.Type):
def serialize(self, value):
if value is None or not isinstance(value, Amount):
return None
return {
"currency": value.currency,
"amount": value.amount
}
def deserialize(self, value):
return Amount(value["currency"], decimal.Decimal(str(value["amount"])))
class ProductSchema(halogen.Schema):
title = halogen.Attr()
price = halogen.Attr(AmountType())
product = ProductSchema.deserialize({"title": "Pencil", "price": {"currency": "EUR", "amount": 0.30}})
print(product)
结果
{"price": Amount: EUR 0.3, "title": "Pencil"}
反序列化验证错误
在反序列化失败时,halogen 会引发特殊异常(halogen.exceptions.ValidationError)。该异常类具有 __unicode__ 方法,该方法可以渲染供用户轻松跟踪的易于阅读的错误结果。
示例
import halogen
class Hello(halogen.Schema):
hello = halogen.Attr()
try:
result = Hello.deserialize({})
except halogen.exceptions.ValidationError as exc:
print(exc)
结果
{
"errors": [
{
"errors": [
{
"type": "str",
"error": "Missing attribute."
}
],
"attr": "hello"
}
],
"attr": "<root>"
}
在您有嵌套模式并使用 List 的情况下,halogen 还会将索引(从 0 开始计数)添加到列表中,以便您可以看到验证错误的确切位置。
示例
import halogen
class Product(halogen.Schema):
"""A product has a name and quantity."""
name = halogen.Attr()
quantity = halogen.Attr()
class NestedSchema(halogen.Schema):
"""An example nested schema."""
products = halogen.Attr(
halogen.types.List(
Product,
),
default=[],
)
try:
result = NestedSchema.deserialize({
"products": [
{
"name": "name",
"quantity": 1
},
{
"name": "name",
}
]
})
except halogen.exceptions.ValidationError as exc:
print(exc)
结果
{
"errors": [
{
"errors": [
{
"index": 1,
"errors": [
{
"errors": [
{
"type": "str",
"error": "Missing attribute."
}
],
"attr": "quantity"
}
]
}
],
"attr": "products"
}
],
"attr": "<root>"
}
请注意,如果在属性反序列化时发生 ValueError 异常,它将被捕获并以 halogen.exceptions.ValidationError 的形式重新抛出。这是为了消除在反序列化过程中在类型和属性中引发 halogen 特定异常的需要。
提供上下文
在序列化或反序列化对象时,并非所有用于(反)序列化的数据都存在于对象本身中。您可以将此数据作为单独的关键字参数传递给 serialize 或 deserialize,以提供上下文。此上下文将在所有嵌套模式、类型和属性中可用。
序列化示例
class ErrorSchema(halogen.Schema):
message = halogen.Attr(
attr=lambda error, language: error["message"][language]
)
error = ErrorSchema.serialize({
"message": {
"dut": "Ongeldig e-mailadres",
"eng": "Invalid email address"
}
}, language="dut")
print error
结果
{"message": "Ongeldig e-mailadres"}
反序列化示例
import halogen
class Book(halogen.Schema):
@halogen.attr()
def title(obj, language):
return obj['title'][language]
class Author(halogen.Schema):
name = halogen.Attr(attr='author.name')
books = halogen.Attr(
halogen.types.List(Book),
attr='author.books',
)
author = Author.deserialize({
"author": {
"name": "Roald Dahl",
"books": [
{
"title": {
"dut": "De Heksen",
"eng": "The Witches"
}
},
{
"title": {
"dut": "Sjakie en de chocoladefabriek",
"eng": "Charlie and the Chocolate Factory"
}
}
]
}
}, language="eng")
print author
结果
{
"name": "Roald Dahl",
"books": [
{"title": "The Witches"},
{"title": "Charlie and the Chocolate Factory"}
]
}
供应商媒体类型
处理验证和业务逻辑错误与处理 HAL 响应一样重要。Halogen 提供了对供应商错误媒体类型的支持,该媒体类型完全兼容 HAL。
vnd.error
供应商错误(application/vnd.error+json)现在已作为互联网草案发布:draft-vnd-error
该媒体类型正在尝试标准化表示问题的格式,以便许多客户端可以表达和理解。多个反序列化错误可以通过路径属性映射到有效负载的相关键,该属性表示有效负载键的 JSON Pointer,因此可以映射到与该键一起序列化的 UI 元素。
import halogen
from halogen.vnd.error import Error, VNDError
class AuthorSchema(halogen.Schema):
name = halogen.Attr(required=True)
class PublisherSchema(halogen.Schema):
name = halogen.Attr(required=True)
address = halogen.Attr()
class BookSchema(halogen.Schema):
title = halogen.Attr(required=True)
year = halogen.Attr(halogen.types.Int(), required=True)
authors = halogen.Attr(halogen.types.List(AuthorSchema), required=True)
publisher = halogen.Attr(PublisherSchema)
try:
BookSchema.deserialize(
dict(
# title is skipped
year="abc", # Not an integer
authors=[dict(name="John Smith"), dict()], # Second author has no name
publisher=dict(address="Chasey Lane 42, Los Angeles, US"), # No name
),
)
except halogen.exceptions.ValidationError as e:
error = Error.from_validation_exception(e)
>>> error.errors
>>>
[
{"path": "/authors/1/name", "message": "Missing attribute."),
{"path": "/title", "message": "Missing attribute."),
{"path": "/year", "message": "'abc' is not an integer"),
{"path": "/publisher/name", "message": "Missing attribute."),
}
错误可能或可能不与有效负载相关,但有时与另一个资源相关。在这种情况下,错误中应返回关于链接。
{
"_links": {
"about": {"href": "/products/1"}
},
"message": "The product is sold out."
}
i18n
错误消息应国际化并尊重 Accept-Language 和 Content-Language HTTP 头。
联系
如果您有任何问题、错误报告、建议等,请在 GitHub 项目页面 上创建一个问题。
许可证
本软件根据 MIT 许可证 许可。
请参阅 许可证文件
© 2013 Oleg Pidsadnyi,Paylogic 国际及其他人。
变更日志
1.7.0
修复 List 类型在反序列化非列表对象时引发无关错误(或在没有传递字典时完全不引发错误)的问题。
1.6.1
1.6.0版本的热修复:修复序列化/反序列化错误 Schema.serialize(…) 使用了kwargs,其中有一些属性使用了不需要kwargs的可调用对象。
1.6.0
修复了DeprecationWarnings的抛出(youtux)
取消对python 2.6的支持,并添加了对python 3.5、3.6、3.7、3.8的支持
1.5.0
允许将上下文关键字参数传递给反序列化方法(blaise-io)
1.4.1
修复了软件包设置问题(olegpidsadnyi)
1.4.0
支持vnd.error响应(olegpidsadnyi)
1.3.5
添加ISO日期时间类型(moisesribeiro)
1.3.4
可空类型(olegpidsadnyi)
1.3.3
严格验证ISO8601(olegpidsadnyi)
1.3.2
提高序列化性能(youtux)
1.3.1
修复String.deserialize以强制文本类型(olegpidsadnyi)
1.3.0
属性作为装饰器(olegpidsadnyi)
1.2.1
使用datetime.isoformat进行datetime序列化(bubenkoff)
1.1.3
正确处理schema类的继承(bubenkoff)
1.1.2
正确处理String和Int类型的反序列化(bubenkoff)
1.1.1
在Link中添加了弃用属性(olegpidsadnyi)
1.1.0
添加常用类型(bubenkoff)
1.0.8
正确处理和文档化< span class="docutils literal">required和< span class="docutils literal">default(bubenkoff)
正确获取验证器的比较值(lazy和constant)(bubenkoff)
增加测试覆盖率(bubenkoff)
1.0.6
在属性反序列化中尊重ValueError(bubenkoff)
1.0.4
正确渲染和文档化反序列化错误(bubenkoff)
1.0.3
允许将嵌入字段标记为非必需(mattupstate)
在序列化文档中保留字段顺序(mattupstate)
1.0.0
首次公开发布