跳转到主要内容

SENAITE API

项目描述

senaite.api
  • SENAITE.API: SENAITE核心和插件开发者的瑞士军刀

https://img.shields.io/pypi/v/senaite.api.svg?style=flat-square https://img.shields.io/github/issues-pr/senaite/senaite.api.svg?style=flat-square https://img.shields.io/github/issues/senaite/senaite.api.svg?style=flat-square https://img.shields.io/badge/README-GitHub-blue.svg?style=flat-square

关于

SENAITE API是SENAITE核心和插件开发者的瑞士军刀。它提供了用于SENAITE中常见任务的合理接口,例如对象创建、按ID/UID查找、搜索等。

请参阅doctests以获取更多细节和用法

安装

请遵循Plone 4senaite.lims的安装说明。

要安装SENAITE API,您必须在您的buildout.cfg中的[buildout]部分添加senaite.apieggs列表中。

[buildout]
parts =
    instance
extends =
    http://dist.plone.org/release/4.3.17/versions.cfg
find-links =
    http://dist.plone.org/release/4.3.17
    http://dist.plone.org/thirdparty
eggs =
    Plone
    Pillow
    senaite.lims
    senaite.api
zcml =
eggs-directory = ${buildout:directory}/eggs

[instance]
recipe = plone.recipe.zope2instance
user = admin:admin
http-address = 0.0.0.0:8080
eggs =
    ${buildout:eggs}
zcml =
    ${buildout:zcml}

[versions]
setuptools =
zc.buildout =

注意

以上示例适用于由统一安装程序创建的buildout。但是,如果您有一个自定义的buildout,您可能需要在[instance]部分而不是在[buildout]部分中添加egg到eggs列表。

有关更多详细信息,请参阅Plone文档的这一部分:https://docs.plone.org/4/en/manage/installing/installing_addons.html

重要

要使更改生效,您需要从您的控制台重新运行buildout

bin/buildout

SENAITE API DOCTEST

SENAITE LIMS API为单一目的提供单一功能。此测试完全基于API,无需任何其他导入。

从buildout目录运行此测试

bin/test test_doctests -t API

简介

该API的目的是帮助开发者遵循DRY原则(不要重复自己)。它还确保使用最有效、最高效的方法来完成任务。

首先导入它

>>> from senaite import api

获取门户

门户是SENAITE LIMS的根对象

>>> portal = api.get_portal()
>>> portal
<PloneSite at /plone>

获取设置对象

设置对象提供对Bika配置设置的访问权限

>>> bika_setup = api.get_setup()
>>> bika_setup
<BikaSetup at /plone/bika_setup>

创建新内容

在Bika LIMS中创建新内容需要一些特殊知识。此功能有助于正确完成并为您创建内容。

在这里,我们在plone/clients文件夹中创建一个新的客户

>>> client = api.create(portal.clients, "Client", title="Test Client")
>>> client
<Client at /plone/clients/client-1>

 >>> client.Title()
 'Test Client'

获取工具

Bika LIMS / Plone中有许多获取工具的方法。此功能将此功能集中起来,使其变得轻松简单。

>>> api.get_tool("bika_setup_catalog")
<BikaSetupCatalog at /plone/bika_setup_catalog>

尝试获取一个不存在的工具会引发自定义的SenaiteAPIError

>>> api.get_tool("NotExistingTool")
Traceback (most recent call last):
[...]
SenaiteAPIError: No tool named 'NotExistingTool' found.

此错误也可用于具有< cite>fail函数的自定义方法。

>>> api.fail("This failed badly")
Traceback (most recent call last):
[...]
SenaiteAPIError: This failed badly

获取对象

从目录大脑中获取工具是Bika LIMS中的一项常见任务。此功能提供了一个统一的接口,用于访问门户对象和大脑。此外,它具有幂等性,因此可以连续多次调用。

我们将演示如何在上文创建的客户对象上使用它

>>> api.get_object(client)
<Client at /plone/clients/client-1>

>>> api.get_object(api.get_object(client))
<Client at /plone/clients/client-1>

现在我们用目录结果展示它

>>> portal_catalog = api.get_tool("portal_catalog")
>>> brains = portal_catalog(portal_type="Client")
>>> brains
[<Products.ZCatalog.Catalog.mybrains object at 0x...>]

>>> brain = brains[0]

>>> api.get_object(brain)
<Client at /plone/clients/client-1>

>>> api.get_object(api.get_object(brain))
<Client at /plone/clients/client-1>

不支持的对象会引发错误

>>> api.get_object(object())
Traceback (most recent call last):
[...]
SenaiteAPIError: <object object at 0x...> is not supported.

要检查一个对象是否受支持,例如是否是ATCT、Dexterity、ZCatalog或门户对象,我们可以使用< cite>is_object函数。

  >>> api.is_object(client)
  True

  >>> api.is_object(brain)
  True

  >>> api.is_object(api.get_portal())
  True

  >>> api.is_object(None)
  False

>>> api.is_object(object())
  False

检查对象是否为门户

有时检查当前对象是否是门户会很方便。

>>> api.is_portal(portal)
True

>>> api.is_portal(client)
False

>>> api.is_portal(object())
False

检查对象是否为目录大脑

知道我们有一个对象或大脑可能会有所帮助。此功能为您检查这一点。

>>> api.is_brain(brain)
True

>>> api.is_brain(api.get_object(brain))
False

>>> api.is_brain(object())
False

检查对象是否为Dexterity内容

此功能检查对象是否为< cite>Dexterity内容类型。

>>> api.is_dexterity_content(client)
False

>>> api.is_dexterity_content(portal)
False

我们目前没有< cite>Dexterity内容,因此测试此功能将在以后进行。

检查对象是否为AT内容

此功能检查对象是否为< cite>Archetypes内容类型。

>>> api.is_at_content(client)
True

>>> api.is_at_content(portal)
False

>>> api.is_at_content(object())
False

获取内容的模式

模式包含内容对象的字段。获取模式是一项常见任务,但根据< cite>ATContentType基于的对象和< cite>Dexterity基于的对象而有所不同。此功能将其统一起来。

>>> schema = api.get_schema(client)
>>> schema
<Products.Archetypes.Schema.Schema object at 0x...>

目录大脑也受支持

>>> api.get_schema(brain)
<Products.Archetypes.Schema.Schema object at 0x...>

获取内容的字段

字段包含对象持有的所有值,因此负责获取和设置信息。

此函数以字典映射< cite>{“key”:value}返回字段。

>>> fields = api.get_fields(client)
>>> fields.get("ClientID")
<Field ClientID(string:rw)>

目录大脑也受支持

>>> api.get_fields(brain).get("ClientID")
<Field ClientID(string:rw)>

获取内容的ID

获取ID是Bika LIMS中的常见任务。此功能确保不会唤醒目录大脑来完成此任务。

>>> api.get_id(portal)
'plone'

>>> api.get_id(client)
'client-1'

>>> api.get_id(brain)
'client-1'

获取内容的标题

获取标题是Bika LIMS中的常见任务。此功能确保不会唤醒目录大脑来完成此任务。

>>> api.get_title(portal)
u'Plone site'

>>> api.get_title(client)
'Test Client'

>>> api.get_title(brain)
'Test Client'

获取内容的描述

获取描述是Bika LIMS中的常见任务。此功能确保不会唤醒目录大脑来完成此任务。

>>> api.get_description(portal)
''

>>> api.get_description(client)
''

>>> api.get_description(brain)
''

获取内容的UID

获取UID是Bika LIMS中的常见任务。此功能确保不会唤醒目录大脑来完成此任务。

实际上,门户对象实际上没有UID。因此,此函数将其定义为< cite>0

>>> api.get_uid(portal)
'0'

>>> uid_client = api.get_uid(client)
>>> uid_client_brain = api.get_uid(brain)
>>> uid_client is uid_client_brain
True

获取内容的URL

获取URL是Bika LIMS中的常见任务。此功能确保不会唤醒目录大脑来完成此任务。

>>> api.get_url(portal)
'http://nohost/plone'

>>> api.get_url(client)
'http://nohost/plone/clients/client-1'

>>> api.get_url(brain)
'http://nohost/plone/clients/client-1'

获取内容的图标

>>> api.get_icon(client)
'<img width="16" height="16" src="http://nohost/plone/++resource++bika.lims.images/client.png" title="Test Client" />'

>>> api.get_icon(brain)
'<img width="16" height="16" src="http://nohost/plone/++resource++bika.lims.images/client.png" title="Test Client" />'

>>> api.get_icon(client, html_tag=False)
'http://nohost/plone/++resource++bika.lims.images/client.png'

>>> api.get_icon(client, html_tag=False)
'http://nohost/plone/++resource++bika.lims.images/client.png'

通过UID获取对象

此功能通过其唯一的ID(UID)查找对象。还支持具有定义好的UID为“0”的门户对象。

>>> api.get_object_by_uid('0')
<PloneSite at /plone>

>>> api.get_object_by_uid(uid_client)
<Client at /plone/clients/client-1>

>>> api.get_object_by_uid(uid_client_brain)
<Client at /plone/clients/client-1>

如果提供了默认值,函数将永远不会失败。任何异常或错误都将导致返回默认值

>>> api.get_object_by_uid('invalid uid', 'default')
'default'

>>> api.get_object_by_uid(None, 'default')
'default'

通过路径获取对象

此函数通过对象的物理路径找到对象

>>> api.get_object_by_path('/plone')
<PloneSite at /plone>

>>> api.get_object_by_path('/plone/clients/client-1')
<Client at /plone/clients/client-1>

门户外部的路径将引发错误

>>> api.get_object_by_path('/root')
Traceback (most recent call last):
[...]
SenaiteAPIError: Not a physical path inside the portal.

任何异常都会返回默认值

>>> api.get_object_by_path('/invaid/path', 'default')
'default'

>>> api.get_object_by_path(None, 'default')
'default'

获取对象的物理路径

物理路径精确描述了对象在门户内部的位置。此函数统一了获取物理路径的不同方法,并以最有效的方式执行

>>> api.get_path(portal)
'/plone'

>>> api.get_path(client)
'/plone/clients/client-1'

>>> api.get_path(brain)
'/plone/clients/client-1'

>>> api.get_path(object())
Traceback (most recent call last):
[...]
SenaiteAPIError: <object object at 0x...> is not supported.

获取对象的物理父路径

此函数返回父对象的物理路径

>>> api.get_parent_path(client)
'/plone/clients'

>>> api.get_parent_path(brain)
'/plone/clients'

然而,此函数仅向上到门户对象

>>> api.get_parent_path(portal)
'/plone'

与其他函数一样,只支持门户对象

>>> api.get_parent_path(object())
Traceback (most recent call last):
[...]
SenaiteAPIError: <object object at 0x...> is not supported.

获取父对象

此函数返回父对象

>>> api.get_parent(client)
<ClientFolder at /plone/clients>

也支持大脑

>>> api.get_parent(brain)
<ClientFolder at /plone/clients>

如果传递的参数 catalog_search 设置为 true,则该函数还可以在 portal_catalog 上执行目录查询并返回一个大脑。

>>> api.get_parent(client, catalog_search=True)
<Products.ZCatalog.Catalog.mybrains object at 0x...>

>>> api.get_parent(brain, catalog_search=True)
<Products.ZCatalog.Catalog.mybrains object at 0x...>

然而,此函数仅向上到门户对象

>>> api.get_parent(portal)
<PloneSite at /plone>

与其他函数一样,只支持门户对象

>>> api.get_parent(object())
Traceback (most recent call last):
[...]
SenaiteAPIError: <object object at 0x...> is not supported.

搜索对象

在 Bika LIMS 中搜索需要了解对象索引在哪个目录中。此函数将所有 Bika LIMS 目录统一到一个单独的搜索界面

>>> results = api.search({'portal_type': 'Client'})
>>> results
[<Products.ZCatalog.Catalog.mybrains object at 0x...>]

也支持多种内容类型

>>> results = api.search({'portal_type': ['Client', 'ClientFolder'], 'sort_on': 'getId'})
>>> map(api.get_id, results)
['client-1', 'clients']

现在我们创建一些位于 bika_setup_catalog 中的对象

>>> instruments = bika_setup.bika_instruments
>>> instrument1 = api.create(instruments, "Instrument", title="Instrument-1")
>>> instrument2 = api.create(instruments, "Instrument", title="Instrument-2")
>>> instrument3 = api.create(instruments, "Instrument", title="Instrument-3")

>>> results = api.search({'portal_type': 'Instrument', 'sort_on': 'getId'})
>>> len(results)
3

>>> map(api.get_id, results)
['instrument-1', 'instrument-2', 'instrument-3']

导致多个目录的查询将被拒绝,因为这需要在之后手动合并和排序结果。因此,我们在这里失败

>>> results = api.search({'portal_type': ['Client', 'ClientFolder', 'Instrument'], 'sort_on': 'getId'})
Traceback (most recent call last):
[...]
SenaiteAPIError: Multi Catalog Queries are not supported, please specify a catalog.

没有 portal_type 的目录查询默认为 portal_catalog,它将找不到以下项目

>>> analysiscategories = bika_setup.bika_analysiscategories
>>> analysiscategory1 = api.create(analysiscategories, "AnalysisCategory", title="AC-1")
>>> analysiscategory2 = api.create(analysiscategories, "AnalysisCategory", title="AC-2")
>>> analysiscategory3 = api.create(analysiscategories, "AnalysisCategory", title="AC-3")

>>> results = api.search({"id": "analysiscategory-1"})
>>> len(results)
0

如果我们添加 portal_type,搜索函数将向 archetype_tool 请求正确的目录,并返回结果

>>> results = api.search({"portal_type": "AnalysisCategory", "id": "analysiscategory-1"})
>>> len(results)
1

我们还可以显式定义一个目录来实现相同的结果

>>> results = api.search({"id": "analysiscategory-1"}, catalog="bika_setup_catalog")
>>> len(results)
1

要查看不活动或休眠的项目,我们必须显式查询它们或在之后手动过滤

>>> results = api.search({"portal_type": "AnalysisCategory", "id": "analysiscategory-1"})
>>> len(results)
1

现在我们停用该项目

>>> analysiscategory1 = api.do_transition_for(analysiscategory1, 'deactivate')
>>> api.is_active(analysiscategory1)
False

搜索仍然可以找到该项目

>>> results = api.search({"portal_type": "AnalysisCategory", "id": "analysiscategory-1"})
>>> len(results)
1

除非我们手动过滤它

>>> len(filter(api.is_active, results))
0

或提供一个正确的查询

>>> results = api.search({"portal_type": "AnalysisCategory", "id": "analysiscategory-1", "inactive_status": "active"})
>>> len(results)
1

获取已注册的目录

Bika LIMS 使用多个通过架构工具注册的目录。此函数返回大脑或对象的已注册目录列表

>>> api.get_catalogs_for(client)
[<CatalogTool at /plone/portal_catalog>]

>>> api.get_catalogs_for(instrument1)
[<BikaSetupCatalog at /plone/bika_setup_catalog>, <CatalogTool at /plone/portal_catalog>]

>>> api.get_catalogs_for(analysiscategory1)
[<BikaSetupCatalog at /plone/bika_setup_catalog>]

获取对象的属性

此函数将属性和方法同等对待,并返回它们的值。它还处理安全并能够返回默认值而不是引发 Unauthorized 错误

>>> uid_brain = api.safe_getattr(brain, "UID")
>>> uid_obj = api.safe_getattr(client, "UID")

>>> uid_brain == uid_obj
True

>>> api.safe_getattr(brain, "review_state")
'active'

>>> api.safe_getattr(brain, "NONEXISTING")
Traceback (most recent call last):
[...]
SenaiteAPIError: Attribute 'NONEXISTING' not found.

>>> api.safe_getattr(brain, "NONEXISTING", "")
''

获取门户目录

这个工具经常需要,所以这个函数只返回它

>>> api.get_portal_catalog()
<CatalogTool at /plone/portal_catalog>

获取对象的审核历史

审核历史提供了关于对象工作流程变化的信息

>>> review_history = api.get_review_history(client)
>>> sorted(review_history[0].items())
[('action', None), ('actor', 'test_user_1_'), ('comments', ''), ('review_state', 'active'), ('time', DateTime('...'))]

获取对象的修订历史

审核历史提供了关于对象工作流程变化的信息

>>> revision_history = api.get_revision_history(client)
>>> sorted(revision_history[0])
['action', 'actor', 'actor_home', 'actorid', 'comments', 'review_state', 'state_title', 'time', 'transition_title', 'type']
>>> revision_history[0]["transition_title"]
u'Create'

获取对象的分配工作流程

此函数返回给定对象的全部分配工作流程

>>> api.get_workflows_for(bika_setup)
('bika_one_state_workflow',)

>>> api.get_workflows_for(client)
('bika_client_workflow', 'bika_inactive_workflow')

此函数也支持将 portal_type 作为参数

>>> api.get_workflows_for(api.get_portal_type(client))
('bika_client_workflow', 'bika_inactive_workflow')

获取对象的流程状态

此函数返回给定对象的状态

>>> api.get_workflow_status_of(client)
'active'

它还具有获取其他状态变量的状态的能力

>>> api.get_workflow_status_of(client, "inactive_state")
'active'

停用客户端

>>> api.do_transition_for(client, "deactivate")
<Client at /plone/clients/client-1>

>>> api.get_workflow_status_of(client, "inactive_state")
'inactive'

>>> api.get_workflow_status_of(client)
'active'

重新激活客户端

>>> api.do_transition_for(client, "activate")
<Client at /plone/clients/client-1>

>>> api.get_workflow_status_of(client, "inactive_state")
'active'

获取对象的可用转换

此函数返回对象工作流程链中所有工作流程的所有可能转换

让我们创建一个批次。它应该允许我们从两个工作流程中调用转换;从 bika_batch_workflow 的“关闭”,以及从 bika_cancellation_workflow 的“取消”

>>> batch1 = api.create(portal.batches, "Batch", title="Test Batch")
>>> transitions = api.get_transitions_for(batch1)
>>> len(transitions)
2

转换作为字典列表返回。由于我们无法依赖于字典键的顺序,我们在这里只能满足自己检查两个预期的转换是否存在于返回值中

>>> 'Close' in [t['title'] for t in transitions]
True
>>> 'Cancel' in [t['title'] for t in transitions]
True

获取对象的创建日期

此函数返回给定对象的创建日期

>>> created = api.get_creation_date(client)
>>> created
DateTime('...')

获取对象的修改日期

此函数返回指定对象的修改日期

>>> modified = api.get_modification_date(client)
>>> modified
DateTime('...')

获取对象的审查状态

此函数返回指定对象的审查状态

>>> review_state = api.get_review_status(client)
>>> review_state
'active'

它也适用于目录大脑

>>> portal_catalog = api.get_tool("portal_catalog")
>>> results = portal_catalog({"portal_type": "Client", "UID": api.get_uid(client)})
>>> len(results)
1
>>> api.get_review_status(results[0]) == review_state
True

获取对象的注册目录

此函数返回给定 portal_type 或对象在 archetype_tool 中注册的所有目录的列表

>>> api.get_catalogs_for(client)
[<CatalogTool at /plone/portal_catalog>]

它也支持将 portal_type 作为参数

>>> api.get_catalogs_for("Analysis")
[<BikaAnalysisCatalog at /plone/bika_analysis_catalog>]

对象转换

此函数执行工作流转换并返回对象

>>> client = api.do_transition_for(client, "deactivate")
>>> api.is_active(client)
False

>>> client = api.do_transition_for(client, "activate")
>>> api.is_active(client)
True

获取不同工作流的非活动/取消状态

有两种工作流允许对象设置为非活动状态。我们提供 is_active 函数,如果使用这两种工作流中的任何一种将项目设置为非活动状态,它将返回 False。

在上面的 search() 测试中,测试了 is_active 函数对 brain 状态的处理。在这里,我只是想测试对象状态是否被正确处理。

对于设置类型,我们使用 bika_inctive_workflow

>>> method1 = api.create(portal.methods, "Method", title="Test Method")
>>> api.is_active(method1)
True
>>> method1 = api.do_transition_for(method1, 'deactivate')
>>> api.is_active(method1)
False

对于事务类型,使用 bika_cancellation_workflow

>>> batch1 = api.create(portal.batches, "Batch", title="Test Batch")
>>> api.is_active(batch1)
True
>>> batch1 = api.do_transition_for(batch1, 'cancel')
>>> api.is_active(batch1)
False

获取对象上特定权限的授权角色

此函数返回一组角色列表,这些角色被授予为传入对象提供的给定权限

>>> api.get_roles_for_permission("Modify portal content", bika_setup)
['LabManager', 'Manager']

检查对象是否可版本化

一些 Bika LIMS 中的内容支持版本控制。此函数为您检查这一点。

仪器不可版本化

>>> api.is_versionable(instrument1)
False

分析服务是可版本化的

>>> analysisservices = bika_setup.bika_analysisservices
>>> analysisservice1 = api.create(analysisservices, "AnalysisService", title="AnalysisService-1")
>>> analysisservice2 = api.create(analysisservices, "AnalysisService", title="AnalysisService-2")
>>> analysisservice3 = api.create(analysisservices, "AnalysisService", title="AnalysisService-3")

>>> api.is_versionable(analysisservice1)
True

获取对象的版本

此函数返回版本作为整数

>>> api.get_version(analysisservice1)
0

调用 processForm 会增加版本号

>>> analysisservice1.processForm()
>>> api.get_version(analysisservice1)
1

获取浏览器视图

获取浏览器视图是 Bika LIMS 中的常见任务

>>> api.get_view("plone")
<Products.Five.metaclass.Plone object at 0x...>

>>> api.get_view("workflow_action")
<Products.Five.metaclass.WorkflowAction object at 0x...>

获取请求

此函数将返回全局请求对象

>>> api.get_request()
<HTTPRequest, URL=http://nohost>

获取一个组

Bika LIMS 中的用户按组管理。一个常见的组是 Clients 组,其中所有客户端联系人用户都分组。此函数提供方便的访问,也是幂等的

>>> clients_group = api.get_group("Clients")
>>> clients_group
<GroupData at /plone/portal_groupdata/Clients used for /plone/acl_users/source_groups>

>>> api.get_group(clients_group)
<GroupData at /plone/portal_groupdata/Clients used for /plone/acl_users/source_groups>

找不到不存在的组

>>> api.get_group("NonExistingGroup")

获取一个用户

可以通过用户 ID 获取用户。此函数是幂等的,并且可以处理用户对象

>>> from plone.app.testing import TEST_USER_ID
>>> user = api.get_user(TEST_USER_ID)
>>> user
<MemberData at /plone/portal_memberdata/test_user_1_ used for /plone/acl_users>

>>> api.get_user(api.get_user(TEST_USER_ID))
<MemberData at /plone/portal_memberdata/test_user_1_ used for /plone/acl_users>

找不到不存在的用户

>>> api.get_user("NonExistingUser")

获取用户属性

用户属性,如电子邮件或全名,作为用户属性存储。这意味着它们不在用户对象上。此函数为您检索这些属性

>>> properties = api.get_user_properties(TEST_USER_ID)
>>> sorted(properties.items())
[('description', ''), ('email', ''), ('error_log_update', 0.0), ('ext_editor', False), ...]

>>> sorted(api.get_user_properties(user).items())
[('description', ''), ('email', ''), ('error_log_update', 0.0), ('ext_editor', False), ...]

如果没有找到用户,则返回一个空的属性字典

>>> api.get_user_properties("NonExistingUser")
{}

>>> api.get_user_properties(None)
{}

通过他们的角色获取用户

>>> from operator import methodcaller

Bika LIMS 中的角色基本上是一组或多组权限的名称。例如,LabManager 描述了一个被授予最多权限的角色。

要查看哪些用户被授予特定角色,您可以使用此函数

>>> labmanagers = api.get_users_by_roles(["LabManager"])
>>> sorted(labmanagers, key=methodcaller('getId'))
[<PloneUser 'test_labmanager'>, <PloneUser 'test_labmanager1'>, <PloneUser 'test-user'>]

也可以向此函数传递单个值

>>> sorted(api.get_users_by_roles("LabManager"), key=methodcaller('getId'))
[<PloneUser 'test_labmanager'>, <PloneUser 'test_labmanager1'>, <PloneUser 'test-user'>]

获取当前用户

获取当前登录用户

>>> api.get_current_user()
<MemberData at /plone/portal_memberdata/test_user_1_ used for /plone/acl_users>

获取与 Plone 用户关联的联系人

获取之前已注册但没有分配联系人的 Plone 用户

>>> user = api.get_user('test_labmanager1')
>>> contact = api.get_user_contact(user)
>>> contact is None
True

为此用户分配新的联系人

>>> labcontacts = bika_setup.bika_labcontacts
>>> labcontact = api.create(labcontacts, "LabContact", Firstname="Lab", Lastname="Manager")
>>> labcontact.setUser(user)
True

然后获取与用户关联的联系人

>>> api.get_user_contact(user)
<LabContact at /plone/bika_setup/bika_labcontacts/labcontact-1>

以及如果我们仅指定 LabContact 类型

>>> api.get_user_contact(user, ['LabContact'])
<LabContact at /plone/bika_setup/bika_labcontacts/labcontact-1>

但如果我们仅指定 Contact 类型则失败

>>> nuser = api.get_user_contact(user, ['Contact'])
>>> nuser is None
True

创建缓存键

此函数为通用对象或 brain 创建一个良好的缓存键

>>> key1 = api.get_cache_key(client)
>>> key1
'Client-client-1-...'

这也可以用于目录结果 brain

>>> portal_catalog = api.get_tool("portal_catalog")
>>> brains = portal_catalog({"portal_type": "Client", "UID": api.get_uid(client)})
>>> key2 = api.get_cache_key(brains[0])
>>> key2
'Client-client-1-...'

这两个键应该相等

>>> key1 == key2
True

当对象被修改时,键应该改变

>>> from zope.lifecycleevent import modified
>>> client.setClientID("TESTCLIENT")
>>> modified(client)
>>> portal.aq_parent._p_jar.sync()
>>> key3 = api.get_cache_key(client)
>>> key3 != key1
True

工作流转换也应当更改缓存键

>>> _ = api.do_transition_for(client, transition="deactivate")
>>> api.get_inactive_status(client)
'inactive'
>>> key4 = api.get_cache_key(client)
>>> key4 != key3
True

缓存键装饰器

此装饰器可用于类中的plone.memoize缓存装饰器。装饰器期望第一个参数是类实例(self),第二个参数是脑图或对象

>>> from plone.memoize.volatile import cache

>>> class BikaClass(object):
...     @cache(api.bika_cache_key_decorator)
...     def get_very_expensive_calculation(self, obj):
...         print "very expensive calculation"
...         return "calculation result"

调用类的(昂贵的)方法只计算一次

>>> instance = BikaClass()
>>> instance.get_very_expensive_calculation(client)
very expensive calculation
'calculation result'
>>> instance.get_very_expensive_calculation(client)
'calculation result'

装饰器也可以处理脑图

>>> instance = BikaClass()
>>> portal_catalog = api.get_tool("portal_catalog")
>>> brain = portal_catalog(portal_type="Client")[0]
>>> instance.get_very_expensive_calculation(brain)
very expensive calculation
'calculation result'
>>> instance.get_very_expensive_calculation(brain)
'calculation result'

ID规范化器

将字符串规范化为可用的系统ID

>>> api.normalize_id("My new ID")
'my-new-id'

>>> api.normalize_id("Really/Weird:Name;")
'really-weird-name'

>>> api.normalize_id(None)
Traceback (most recent call last):
[...]
SenaiteAPIError: Type of argument must be string, found '<type 'NoneType'>'

文件规范化器

将字符串规范化为可用的文件名

>>> api.normalize_filename("My new ID")
'My new ID'

>>> api.normalize_filename("Really/Weird:Name;")
'Really-Weird-Name'

>>> api.normalize_filename(None)
Traceback (most recent call last):
[...]
SenaiteAPIError: Type of argument must be string, found '<type 'NoneType'>'

检查UID是否有效

检查UID是否为有效的23位字母数字UID

>>> api.is_uid("ajw2uw9")
False

>>> api.is_uid(None)
False

>>> api.is_uid("")
False

>>> api.is_uid("0")
False

>>> api.is_uid('0e1dfc3d10d747bf999948a071bc161e')
True

检查UID是否为有效的23位字母数字UID,并带有脑图

>>> api.is_uid("ajw2uw9", validate=True)
False

>>> api.is_uid(None, validate=True)
False

>>> api.is_uid("", validate=True)
False

>>> api.is_uid("0", validate=True)
False

>>> api.is_uid('0e1dfc3d10d747bf999948a071bc161e', validate=True)
False

>>> asfolder = self.portal.bika_setup.bika_analysisservices
>>> serv = api.create(asfolder, "AnalysisService", title="AS test")
>>> serv.setKeyword("as_test")
>>> uid = serv.UID()
>>> api.is_uid(uid, validate=True)
True

检查日期是否有效

先进行一些导入

>>> from datetime import datetime
>>> from DateTime import DateTime

检查DateTime是否有效

>>> now = DateTime()
>>> api.is_date(now)
True

>>> now = datetime.now()
>>> api.is_date(now)
True

>>> now = DateTime(now)
>>> api.is_date(now)
True

>>> api.is_date(None)
False

>>> api.is_date('2018-04-23')
False

尝试转换为日期

尝试转换为DateTime

>>> now = DateTime()
>>> zpdt = api.to_date(now)
>>> zpdt.ISO8601() == now.ISO8601()
True

>>> now = datetime.now()
>>> zpdt = api.to_date(now)
>>> pydt = zpdt.asdatetime()

请注意,在这里,对于日期的比较,我们将DateTime转换为python datetime,因为DateTime.strftime()对于时区是损坏的(总是查看系统时区,忽略DateTime实例本身的时区和偏移量)

>>> pydt.strftime('%Y-%m-%dT%H:%M:%S') == now.strftime('%Y-%m-%dT%H:%M:%S')
True

尝试相同,但使用utcnow()代替

>>> now = datetime.utcnow()
>>> zpdt = api.to_date(now)
>>> pydt = zpdt.asdatetime()
>>> pydt.strftime('%Y-%m-%dT%H:%M:%S') == now.strftime('%Y-%m-%dT%H:%M:%S')
True

现在我们只转换格式化的日期字符串

>>> strd = "2018-12-01 17:50:34"
>>> zpdt = api.to_date(strd)
>>> zpdt.ISO8601()
'2018-12-01T17:50:34'

现在我们只转换格式化的日期字符串,但带有时区

>>> strd = "2018-12-01 17:50:34 GMT+1"
>>> zpdt = api.to_date(strd)
>>> zpdt.ISO8601()
'2018-12-01T17:50:34+01:00'

我们也检查一个错误的日期(注意月份是13)

>>> strd = "2018-13-01 17:50:34"
>>> zpdt = api.to_date(strd)
>>> api.is_date(zpdt)
False

并且使用欧洲格式

>>> strd = "01.12.2018 17:50:34"
>>> zpdt = api.to_date(strd)
>>> zpdt.ISO8601()
'2018-12-01T17:50:34'

>>> zpdt = api.to_date(None)
>>> zpdt is None
True

使用格式化的日期字符串作为后备

>>> strd = "2018-13-01 17:50:34"
>>> default_date = "2018-01-01 19:30:30"
>>> zpdt = api.to_date(strd, default_date)
>>> zpdt.ISO8601()
'2018-01-01T19:30:30'

使用DateTime对象作为后备

>>> strd = "2018-13-01 17:50:34"
>>> default_date = "2018-01-01 19:30:30"
>>> default_date = api.to_date(default_date)
>>> zpdt = api.to_date(strd, default_date)
>>> zpdt.ISO8601() == default_date.ISO8601()
True

使用datetime对象作为后备

>>> strd = "2018-13-01 17:50:34"
>>> default_date = datetime.now()
>>> zpdt = api.to_date(strd, default_date)
>>> dzpdt = api.to_date(default_date)
>>> zpdt.ISO8601() == dzpdt.ISO8601()
True

使用不可转换的值作为后备

>>> strd = "2018-13-01 17:50:34"
>>> default_date = "something wrong here"
>>> zpdt = api.to_date(strd, default_date)
>>> zpdt is None
True

检查是否可浮点化

>>> api.is_floatable(None)
False

>>> api.is_floatable("")
False

>>> api.is_floatable("31")
True

>>> api.is_floatable("31.23")
True

>>> api.is_floatable("-13")
True

>>> api.is_floatable("12,35")
False

转换为浮点数

>>> api.to_float("2")
2.0

>>> api.to_float("2.234")
2.234

带有默认后备

>>> api.to_float(None, 2)
2.0

>>> api.to_float(None, "2")
2.0

>>> api.to_float("", 2)
2.0

>>> api.to_float("", "2")
2.0

>>> api.to_float(2.1, 2)
2.1

>>> api.to_float("2.1", 2)
2.1

>>> api.to_float("2.1", "2")
2.1

API分析

api_analysis提供与分析相关特定目的的单个函数

从buildout目录运行此测试

bin/test test_textual_doctests -t API_analysis

测试设置

需要的导入

>>> import re
>>> from AccessControl.PermissionRole import rolesForPermissionOn
>>> from bika.lims import api
>>> from bika.lims.api.analysis import is_out_of_range
>>> from bika.lims.content.analysisrequest import AnalysisRequest
>>> from bika.lims.content.sample import Sample
>>> from bika.lims.content.samplepartition import SamplePartition
>>> from bika.lims.utils.analysisrequest import create_analysisrequest
>>> from bika.lims.utils.sample import create_sample
>>> from bika.lims.utils import tmpID
>>> from bika.lims.workflow import doActionFor
>>> from bika.lims.workflow import getCurrentState
>>> from bika.lims.workflow import getAllowedTransitions
>>> from DateTime import DateTime
>>> from plone.app.testing import TEST_USER_ID
>>> from plone.app.testing import TEST_USER_PASSWORD
>>> from plone.app.testing import setRoles

功能性辅助工具

>>> def start_server():
...     from Testing.ZopeTestCase.utils import startZServer
...     ip, port = startZServer()
...     return "http://{}:{}/{}".format(ip, port, portal.id)

变量

>>> portal = self.portal
>>> request = self.request
>>> bikasetup = portal.bika_setup

我们需要为测试创建一些基本对象

>>> setRoles(portal, TEST_USER_ID, ['LabManager',])
>>> date_now = DateTime().strftime("%Y-%m-%d")
>>> date_future = (DateTime() + 5).strftime("%Y-%m-%d")
>>> client = api.create(portal.clients, "Client", Name="Happy Hills", ClientID="HH", MemberDiscountApplies=True)
>>> contact = api.create(client, "Contact", Firstname="Rita", Lastname="Mohale")
>>> sampletype = api.create(bikasetup.bika_sampletypes, "SampleType", title="Water", Prefix="W")
>>> labcontact = api.create(bikasetup.bika_labcontacts, "LabContact", Firstname="Lab", Lastname="Manager")
>>> department = api.create(bikasetup.bika_departments, "Department", title="Chemistry", Manager=labcontact)
>>> category = api.create(bikasetup.bika_analysiscategories, "AnalysisCategory", title="Metals", Department=department)
>>> supplier = api.create(bikasetup.bika_suppliers, "Supplier", Name="Naralabs")
>>> Cu = api.create(bikasetup.bika_analysisservices, "AnalysisService", title="Copper", Keyword="Cu", Price="15", Category=category.UID(), DuplicateVariation="0.5")
>>> Fe = api.create(bikasetup.bika_analysisservices, "AnalysisService", title="Iron", Keyword="Fe", Price="10", Category=category.UID(), DuplicateVariation="0.5")
>>> Au = api.create(bikasetup.bika_analysisservices, "AnalysisService", title="Gold", Keyword="Au", Price="20", Category=category.UID(), DuplicateVariation="0.5")
>>> Mg = api.create(bikasetup.bika_analysisservices, "AnalysisService", title="Magnesium", Keyword="Mg", Price="20", Category=category.UID(), DuplicateVariation="0.5")
>>> service_uids = [api.get_uid(an) for an in [Cu, Fe, Au, Mg]]

Water创建一个分析规范

>>> sampletype_uid = api.get_uid(sampletype)
>>> rr1 = {"keyword": "Au", "min": "-5", "max":  "5", "warn_min": "-5.5", "warn_max": "5.5"}
>>> rr2 = {"keyword": "Cu", "min": "10", "max": "20", "warn_min":  "9.5", "warn_max": "20.5"}
>>> rr3 = {"keyword": "Fe", "min":  "0", "max": "10", "warn_min": "-0.5", "warn_max": "10.5"}
>>> rr4 = {"keyword": "Mg", "min": "10", "max": "10"}
>>> rr = [rr1, rr2, rr3, rr4]
>>> specification = api.create(bikasetup.bika_analysisspecs, "AnalysisSpec", title="Lab Water Spec", SampleType=sampletype_uid, ResultsRange=rr)
>>> spec_uid = api.get_uid(specification)

为空创建一个参考定义

>>> blankdef = api.create(bikasetup.bika_referencedefinitions, "ReferenceDefinition", title="Blank definition", Blank=True)
>>> blank_refs = [{'uid': Au.UID(), 'result': '0', 'min': '0', 'max': '0'},]
>>> blankdef.setReferenceResults(blank_refs)

并为控制

>>> controldef = api.create(bikasetup.bika_referencedefinitions, "ReferenceDefinition", title="Control definition")
>>> control_refs = [{'uid': Au.UID(), 'result': '10', 'min': '9.99', 'max': '10.01'},
...                 {'uid': Cu.UID(), 'result': '-0.9','min': '-1.08', 'max': '-0.72'},]
>>> controldef.setReferenceResults(control_refs)

>>> blank = api.create(supplier, "ReferenceSample", title="Blank",
...                    ReferenceDefinition=blankdef,
...                    Blank=True, ExpiryDate=date_future,
...                    ReferenceResults=blank_refs)
>>> control = api.create(supplier, "ReferenceSample", title="Control",
...                      ReferenceDefinition=controldef,
...                      Blank=False, ExpiryDate=date_future,
...                      ReferenceResults=control_refs)

创建一个分析请求

>>> values = {
...     'Client': api.get_uid(client),
...     'Contact': api.get_uid(contact),
...     'DateSampled': date_now,
...     'SampleType': sampletype_uid,
...     'Specification': spec_uid,
...     'Priority': '1',
... }

>>> ar = create_analysisrequest(client, request, values, service_uids)
>>> success = doActionFor(ar, 'receive')

创建一个新的工作表并添加分析

>>> worksheet = api.create(portal.worksheets, "Worksheet")
>>> analyses = map(api.get_object, ar.getAnalyses())
>>> for analysis in analyses:
...     worksheet.addAnalysis(analysis)

Cu添加一个重复项

>>> position = worksheet.get_slot_position(ar, 'a')
>>> duplicates = worksheet.addDuplicateAnalyses(position)
>>> duplicates.sort(key=lambda analysis: analysis.getKeyword(), reverse=False)

添加一个空白和控制

>>> blanks = worksheet.addReferenceAnalyses(blank, service_uids)
>>> blanks.sort(key=lambda analysis: analysis.getKeyword(), reverse=False)
>>> controls = worksheet.addReferenceAnalyses(control, service_uids)
>>> controls.sort(key=lambda analysis: analysis.getKeyword(), reverse=False)

检查结果是否超出范围

首先,从插槽1获取分析并按升序排序

>>> analyses = worksheet.get_analyses_at(1)
>>> analyses.sort(key=lambda analysis: analysis.getKeyword(), reverse=False)

为分析Au设置结果(最小值:-5,最大值:5,警告最小值:-5.5,警告最大值:5.5)

>>> au_analysis = analyses[0]
>>> au_analysis.setResult(2)
>>> is_out_of_range(au_analysis)
(False, False)

>>> au_analysis.setResult(-2)
>>> is_out_of_range(au_analysis)
(False, False)

>>> au_analysis.setResult(-5)
>>> is_out_of_range(au_analysis)
(False, False)

>>> au_analysis.setResult(5)
>>> is_out_of_range(au_analysis)
(False, False)

>>> au_analysis.setResult(10)
>>> is_out_of_range(au_analysis)
(True, True)

>>> au_analysis.setResult(-10)
>>> is_out_of_range(au_analysis)
(True, True)

结果在肩部吗?

>>> au_analysis.setResult(-5.2)
>>> is_out_of_range(au_analysis)
(True, False)

>>> au_analysis.setResult(-5.5)
>>> is_out_of_range(au_analysis)
(True, False)

>>> au_analysis.setResult(-5.6)
>>> is_out_of_range(au_analysis)
(True, True)

>>> au_analysis.setResult(5.2)
>>> is_out_of_range(au_analysis)
(True, False)

>>> au_analysis.setResult(5.5)
>>> is_out_of_range(au_analysis)
(True, False)

>>> au_analysis.setResult(5.6)
>>> is_out_of_range(au_analysis)
(True, True)

检查重复结果是否超出范围

获取来自Au的第一个重复分析

>>> duplicate = duplicates[0]

如果重复的结果与从重复分析设置的分析的结果不匹配,则将重复视为超出范围,其中重复变化百分比作为误差范围。分析服务Au分配的重复变化为0.5%

>>> dup_variation = au_analysis.getDuplicateVariation()
>>> dup_variation = api.to_float(dup_variation)
>>> dup_variation
0.5

为常规分析设置一个在范围内(在-5和5之间)的结果,并检查其重复的所有变体。鉴于重复变化为0.5,重复的有效范围必须为Au +-0.5%

>>> result = 2.0
>>> au_analysis.setResult(result)
>>> is_out_of_range(au_analysis)
(False, False)

>>> duplicate.setResult(result)
>>> is_out_of_range(duplicate)
(False, False)

>>> dup_min_range = result - (result*(dup_variation/100))
>>> duplicate.setResult(dup_min_range)
>>> is_out_of_range(duplicate)
(False, False)

>>> duplicate.setResult(dup_min_range - 0.5)
>>> is_out_of_range(duplicate)
(True, True)

>>> dup_max_range = result + (result*(dup_variation/100))
>>> duplicate.setResult(dup_max_range)
>>> is_out_of_range(duplicate)
(False, False)

>>> duplicate.setResult(dup_max_range + 0.5)
>>> is_out_of_range(duplicate)
(True, True)

为常规分析设置一个超出范围但位于肩部的结果,并检查其重复的所有变体。鉴于重复变化为0.5,重复的有效范围必须为Au +-0.5%

>>> result = 5.5
>>> au_analysis.setResult(result)
>>> is_out_of_range(au_analysis)
(True, False)

>>> duplicate.setResult(result)
>>> is_out_of_range(duplicate)
(False, False)

>>> dup_min_range = result - (result*(dup_variation/100))
>>> duplicate.setResult(dup_min_range)
>>> is_out_of_range(duplicate)
(False, False)

>>> duplicate.setResult(dup_min_range - 0.5)
>>> is_out_of_range(duplicate)
(True, True)

>>> dup_max_range = result + (result*(dup_variation/100))
>>> duplicate.setResult(dup_max_range)
>>> is_out_of_range(duplicate)
(False, False)

>>> duplicate.setResult(dup_max_range + 0.5)
>>> is_out_of_range(duplicate)
(True, True)

为常规分析设置一个超出范围且超出肩部的结果,并检查其重复的所有变体。鉴于重复变化为0.5,重复的有效范围必须为Au +-0.5%

>>> result = -7.0
>>> au_analysis.setResult(result)
>>> is_out_of_range(au_analysis)
(True, True)

>>> duplicate.setResult(result)
>>> is_out_of_range(duplicate)
(False, False)

>>> dup_min_range = result - (abs(result)*(dup_variation/100))
>>> duplicate.setResult(dup_min_range)
>>> is_out_of_range(duplicate)
(False, False)

>>> duplicate.setResult(dup_min_range - 0.5)
>>> is_out_of_range(duplicate)
(True, True)

>>> dup_max_range = result + (abs(result)*(dup_variation/100))
>>> duplicate.setResult(dup_max_range)
>>> is_out_of_range(duplicate)
(False, False)

>>> duplicate.setResult(dup_max_range + 0.5)
>>> is_out_of_range(duplicate)
(True, True)

检查参考分析(空白+控制)的结果是否超出范围

参考分析(对照和空白)不使用规范中定义的结果范围,而是使用从其生成的参考样品中定义的结果范围。反过来,参考样品中定义的结果范围可以是手动设置或从可能与之关联的参考定义中获取。与常规分析相比的另一个不同之处在于,参考分析不期望一个有效范围,而是一个离散值,因此肩部是基于%误差构建的。

空白分析

第一个空白分析对应于 Au

>>> au_blank = blanks[0]

对于 Au 空白,根据上面使用的参考定义,预期结果为 0 +/- 0.1%。由于预期结果为 0,无论误差百分比如何,都不会考虑肩部。因此,结果超出范围时始终为“超出肩部”。

>>> au_blank.setResult(0.0)
>>> is_out_of_range(au_blank)
(False, False)

>>> au_blank.setResult("0")
>>> is_out_of_range(au_blank)
(False, False)

>>> au_blank.setResult(0.0001)
>>> is_out_of_range(au_blank)
(True, True)

>>> au_blank.setResult("0.0001")
>>> is_out_of_range(au_blank)
(True, True)

>>> au_blank.setResult(-0.0001)
>>> is_out_of_range(au_blank)
(True, True)

>>> au_blank.setResult("-0.0001")
>>> is_out_of_range(au_blank)
(True, True)

对照分析

第一个对照分析对应于 Au

>>> au_control = controls[0]

对于 Au 对照,根据上面使用的参考定义,预期结果为 10 +/- 0.1% = 10 +/- 0.01

首先,检查范围内的值

>>> au_control.setResult(10)
>>> is_out_of_range(au_control)
(False, False)

>>> au_control.setResult(10.0)
>>> is_out_of_range(au_control)
(False, False)

>>> au_control.setResult("10")
>>> is_out_of_range(au_control)
(False, False)

>>> au_control.setResult("10.0")
>>> is_out_of_range(au_control)
(False, False)

>>> au_control.setResult(9.995)
>>> is_out_of_range(au_control)
(False, False)

>>> au_control.setResult("9.995")
>>> is_out_of_range(au_control)
(False, False)

>>> au_control.setResult(10.005)
>>> is_out_of_range(au_control)
(False, False)

>>> au_control.setResult("10.005")
>>> is_out_of_range(au_control)
(False, False)

>>> au_control.setResult(9.99)
>>> is_out_of_range(au_control)
(False, False)

>>> au_control.setResult("9.99")
>>> is_out_of_range(au_control)
(False, False)

>>> au_control.setResult(10.01)
>>> is_out_of_range(au_control)
(False, False)

>>> au_control.setResult("10.01")
>>> is_out_of_range(au_control)
(False, False)

现在,检查超出范围的结果

>>> au_control.setResult(9.98)
>>> is_out_of_range(au_control)
(True, True)

>>> au_control.setResult("9.98")
>>> is_out_of_range(au_control)
(True, True)

>>> au_control.setResult(10.011)
>>> is_out_of_range(au_control)
(True, True)

>>> au_control.setResult("10.011")
>>> is_out_of_range(au_control)
(True, True)

然后,对预期 -0.9 +/- 20% 的 Cu 对照也进行相同操作

>>> cu_control = controls[1]

首先,检查范围内的值

>>> cu_control.setResult(-0.9)
>>> is_out_of_range(cu_control)
(False, False)

>>> cu_control.setResult("-0.9")
>>> is_out_of_range(cu_control)
(False, False)

>>> cu_control.setResult(-1.08)
>>> is_out_of_range(cu_control)
(False, False)

>>> cu_control.setResult("-1.08")
>>> is_out_of_range(cu_control)
(False, False)

>>> cu_control.setResult(-1.07)
>>> is_out_of_range(cu_control)
(False, False)

>>> cu_control.setResult("-1.07")
>>> is_out_of_range(cu_control)
(False, False)

>>> cu_control.setResult(-0.72)
>>> is_out_of_range(cu_control)
(False, False)

>>> cu_control.setResult("-0.72")
>>> is_out_of_range(cu_control)
(False, False)

>>> cu_control.setResult(-0.73)
>>> is_out_of_range(cu_control)
(False, False)

>>> cu_control.setResult("-0.73")
>>> is_out_of_range(cu_control)
(False, False)

现在,检查超出范围的结果

>>> cu_control.setResult(0)
>>> is_out_of_range(cu_control)
(True, True)

>>> cu_control.setResult("0")
>>> is_out_of_range(cu_control)
(True, True)

>>> cu_control.setResult(-0.71)
>>> is_out_of_range(cu_control)
(True, True)

>>> cu_control.setResult("-0.71")
>>> is_out_of_range(cu_control)
(True, True)

>>> cu_control.setResult(-1.09)
>>> is_out_of_range(cu_control)
(True, True)

>>> cu_control.setResult("-1.09")
>>> is_out_of_range(cu_control)
(True, True)

变更日志

1.2.3 (2018-06-23)

  • 更多 PyPI 配置文件

1.2.2 (2018-06-23)

  • PyPI 文档页面配置文件

1.2.1 (2018-06-23)

  • 更好的 PyPI 文档页面

  • 修复了 Doctests 的格式

1.2.0 (2018-06-23)

添加了

  • 添加了 is_uid 函数

删除了

更改了

  • 添加了 SENAITE CORE API 函数

修复了

  • 修复了测试

安全

1.1.0 (2018-01-03)

添加了

删除了

更改了

  • 许可证更改为 GPLv2

  • 集成到 SENAITE CORE

修复了

  • 修复了测试

安全

1.0.2 (2017-11-24)

  • #397(bika.lims) 修复 Issue-396:AR 发布时的 AttributeError: uid_catalog

1.0.1 (2017-09-30)

  • 修复了损坏的发布(缺少 MANIFEST.in)

1.0.0 (2017-09-30)

  • 第一个版本

项目详情


下载文件

下载适合您平台的文件。如果您不确定要选择哪个,请了解有关 安装软件包 的更多信息。

源分布

senaite.api-1.2.3.post2.zip (65.4 kB 查看散列值)

上传时间

构建分布

senaite.api-1.2.3.post2-py2-none-any.whl (40.4 kB 查看散列值)

上传时间 Python 2

由以下支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误日志 StatusPage StatusPage 状态页面