CodeProject API 的 Python 包装器





3.00/5 (3投票s)
本文介绍了用 Python 编写的 CodeProject API 封装器的工作原理和用法。
引言
在本文中,我将解释我为 CodeProject API[^] 构建的 Python 封装器的工作原理。我构建它的目的是让您可以通过几个方法调用,轻松地从 Python 与 CodeProject API 进行交互,而无需自己编写 API 交互代码。我将介绍如何使用该封装器,以及它在底层的工作原理。该封装器可用于 Python 2 和 3。
CPApiWrapper 类
该封装器包含在 CPApiWrapper
类中,该类包含所有从 API 获取数据和访问令牌的方法。该类有一个成员变量:access_token_data
。这包含访问令牌和一些相关信息(见下一节)。构造函数将此成员的初始值设置为 None
。
def __init__(self):
self.access_token_data = None
类的使用和构造函数
from CPApiWrapper.cpapiwrapper import CPApiWrapper
# ^ assumes that the Python package is in a subdirectory called "CPApiWrapper"
wrapper = CPApiWrapper()
导入语句
值得一看的是 CPApiWrapper.py 顶部的导入语句
import requests
from .exceptions import *
from .itemsummarylistviewmodel import ItemSummaryListViewModel
from .accesstokendata import AccessTokenData
from .mynotificationsviewmodel import MyNotificationsViewModel
from .myprofileviewmodel import MyProfileViewModel
from .myreputationviewmodel import MyReputationViewModel
from .forumdisplaymode import ForumDisplayMode
from .questionlistmode import QuestionListMode
try:
from urlparse import urljoin # Python 2
except ImportError:
from urllib.parse import urljoin # Python 3
第一个导入没什么好说的,它只是导入了 requests
包,它是封装器的依赖项。之后的导入以点开头。这种导入是一种包相对导入:它导入相对于当前包(在本例中为 CPApiWrapper 包)的内容。
最后一个导入导入了 urljoin
方法。此方法在 Python 2 和 Python 3 中位于两个不同的包中:Python 2 中位于 urlparse
,Python 3 中位于 urllib.parse
。为了支持这两个版本,导入被包装在一个 try-except 语句中:如果 Python 2 导入失败,它将执行 Python 3 导入。然后,您可以在代码中的任何地方调用 urljoin
,而无需担心版本问题。
获取访问令牌
您必须先获取访问令牌才能发送任何请求。
获取访问令牌的方法名为 get_access_token
,它需要两个辅助类:AccessTokenData
和 AccessTokenNotFetchedException
AccessTokenData 类
这个类用于保存从 API 返回的访问令牌数据。它有三个类成员
access_token
- 访问令牌,以字符串形式存储。expires_in
- 告知令牌何时过期。token_type
- OAuth 令牌类型。
get_access_token
类将使用这个类来存储 API 响应。构造函数有一个 data
参数。创建 AccessTokenData
实例时,您必须将一个字典作为此参数传递,其中包含所需字段。当此类从 get_access_token
调用时,它将把解析后的 JSON 响应传递给构造函数。
class AccessTokenData():
def __init__(self, data):
self.access_token = data["access_token"]
self.expires_in = data["expires_in"]
self.token_type = data["token_type"]
AccessTokenNotFetchedException
当 get_access_token
未能获取 API 令牌时,会抛出此类型的异常。它继承自 Exception
。它有一个 parsed_json
成员变量,get_access_token
在您捕获异常并想知道 API 响应了什么时,会在此处存储解析后的 API 响应。在 AccessTokenNotFetchedException 构造函数的开头,它调用其超类的 __init__
方法,并以 message
作为参数,以确保传递的消息确实被设置为异常消息。
class AccessTokenNotFetchedException(Exception):
def __init__(self, message, parsed_json):
super(AccessTokenNotFetchedException, self).__init__(message)
self.parsed_json = parsed_json
get_access_token 方法
这是实际获取访问令牌的方法。获取后,它会将 AccessTokenData
实例存储在 access_token_data
成员中。
要从 API 获取访问令牌,您必须提供您的客户端 ID、客户端密钥以及可选的 CodeProject 登录电子邮件和密码。要获取客户端 ID 和客户端密钥,您必须在 CodeProject Web API 客户端设置中注册您的应用程序。传递电子邮件和密码是可选的:API 仅在您需要访问包含您的声誉、文章、问题等信息的“我的 API”时才要求提供。
该方法将在 POST 请求中将提供的客户端 ID、客户端密钥(以及可选的电子邮件和密码)传递给 https://api.codeproject.com/Token
。传递给 API 的数据是 grant_type=client_credentials&client_id=ID&client_secret=secret
(不带电子邮件/密码)和 grant_type=password&client_id=ID&client_secret=secret&username=email&password=password
(带电子邮件和密码)。我们无需自行编码数据。我们使用 requests
库为我们完成此操作:我们只需将包含数据的字典传递给 requests.post
,requests
将负责正确的编码。API 返回包含我们将提供给 AccessTokenData 类的数据的 JSON。如果 JSON 响应具有 "error"
字段,则会抛出 AccessTokenNotFethchedException
。
def get_access_token(self, client_id, client_secret,
email=None, password=None):
if email is None or password is None:
data = {"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret}
else:
data = {"grant_type": "password",
"client_id": client_id,
"client_secret": client_secret,
"username": email,
"password": password}
resp = requests.post("https://api.codeproject.com/Token", data).json()
if "error" in resp:
exc = AccessTokenNotFetchedException(
"Could not fetch API Access Token. Error: " + resp["error"],
resp
)
raise exc
self.access_token_data = AccessTokenData(resp)
发送 API 请求
CPApiWrapper
类有许多方法可以通过一个方法调用获取特定的 API 数据——例如,获取最新问题的第 1 页。所有这些方法都使用一个辅助方法来发送给定 URL 的实际 API 请求,即 api_request
。它接受一个 url
参数和一个 params
参数(默认为 None),用于将查询字符串参数作为字典传递。url
参数必须是相对于 https://api.codeproject.com/v1
的 URL。api_request
使用 urljoin
方法将 URL 连接起来,我们使用 requests
包发送带有提供参数的 GET 请求。如果 API 返回的 JSON 响应中包含“error”或“message”字段,则表示出现问题,该方法将抛出 ApiRequestFailedException
。
ApiRequestFailedException
类的外观和工作方式与上面讨论的 AccessTokenNotFetchedException
类完全相同。它只是名称不同。
class ApiRequestFailedException(Exception):
def __init__(self, message, parsed_json):
super(ApiRequestFailedException, self).__init__(message)
self.parsed_json = parsed_json
这是 api_request
方法
def api_request(self, url, params=None):
if self.access_token_data is None:
exc = AccessTokenNotFetchedException(
"Fetch the API key before sending a request.",
None
)
raise exc
url = url.lstrip("/")
url = urljoin("https://api.codeproject.com/v1/", url)
headers = {'Pragma': 'No-cache',
'Accept': 'application/json',
'Authorization': '{0} {1}'.format(
self.access_token_data.token_type,
self.access_token_data.access_token
)}
if params is not None:
passed_params = {k: v for (k, v) in params.items() if v is not None}
else:
passed_params = None
resp = requests.get(url, headers=headers, params=passed_params)
data = resp.json()
if "message" in data:
exc = ApiRequestFailedException(
"API Request Failed. Message: " + data["message"],
data
)
raise exc
if "error" in data:
exc = ApiRequestFailedException(
"API Request Failed. Message: " + data["error"],
data
)
raise exc
return data
如上代码块所示,我们使用 lstrip
从传入的 url
参数中移除前导斜杠。这是必须的,因为如果相对 API URL 以斜杠开头,封装器会认为它是完全有效的,但如果保留前导斜杠,urljoin
会使相对 URL 相对于根目录 https://api.codeproject.com/
,而不是相对于 https://api.codeproject.com/v1/
。
params
不会直接传递给 requests.get
:这个字典中的某些值可能为 None
,因此不应传递它们。我们使用字典推导过滤掉它们。所有值为非 None
的键值对都将被保留。.items()
在 Python 2 和 Python 3 中具有不同的返回类型,但两者都适用于字典推导。
视图模型
如果您打算使用封装器,您可能不需要上面描述的 api_request
方法。CPApiWrapper
类中有许多方法使用 api_request
方法发送请求,并将响应存储在视图模型中。
这是视图模型的简要概述
NameIdPair
- 名称和 ID 的容器。ItemSummaryListViewModel
-PaginationInfo
对象和ItemSummary
列表的容器。由多个 API 页面返回。ItemSummary
- 包含有关“项目”的数据,可以是问题、论坛消息、文章等。PaginationInfo
- 包含有关分页的信息:当前页码、总页数等。MyNotificationsViewModel
-NotificationViewModel
对象的容器。NotificationViewModel
- 包含有关通知的数据的容器。MyProfileViewModel
- 包含您的个人资料数据,例如声誉、显示名称等。MyReputationViewModel
- 包含您的总声誉以及您在每个类别中的声誉。ReputationTypeViewModel
- 包含有关特定声誉类别的数据:名称、您在该类别中的积分数量等。
NameIdPair
这个视图模型在其他视图模型中非常常用。它有两个成员:name
和 id
。与所有视图模型一样,NameIdPair
有一个接受字典的构造函数,该字典必须是解析后的 API 响应的一部分(或对于某些视图模型,是整个解析后的响应)。
def __init__(self, data):
self.name = data['name']
self.id = data['id']
有时,API 响应的一部分不仅仅是一个 NameIdPair
,而是列表中的多个。这种情况经常发生,因此 NameIdPair
提供了一个静态方法 data_to_pairs
,它将解析后的 API 响应中的字典列表转换为 NameIdPair
类的实例列表。这被放在一个单独的方法中,因为如果由于某种原因需要更改它,就不必在发生此转换的每个地方进行更改,而只需在一个地方进行更改:该方法。
@staticmethod
def data_to_pairs(data):
return [NameIdPair(x) for x in data]
此方法使用列表推导将字典列表转换为 NameIdPairs
列表:对于 data
中的每个 x
,它都采用 NameIdPair(x)
。
ItemSummaryListViewModel
ItemSummaryListViewModel
由最新文章、问题和消息的 API 页面返回。它具有以下类成员
pagination
-PaginationInfo
的实例,顾名思义,它包含有关分页的信息。items
-ItemSummary
数组。
与所有视图模型一样,ItemSumamryListViewModel
有一个接受字典的构造函数,该字典必须是解析后的 API 响应。
from .paginationinfo import PaginationInfo
from .itemsummary import ItemSummary
class ItemSummaryListViewModel():
def __init__(self, data):
self.pagination = PaginationInfo(data['pagination'])
self.items = ItemSummary.data_to_items(data['items'])
data['pagination']
也是一个字典,所以我们将其传递给 PaginationInfo
构造函数。data['items']
是一个字典列表。ItemSummary
的 data_to_items
方法遍历此列表中的所有项目,并返回一个 ItemSummary
列表(参见下一节)。
ItemSummary
此视图模型包含“项目”的摘要:文章/博客/提示、问题、答案或消息。它具有以下成员(其中一些是可选的。如果它们是可选的且在 API 响应中不存在,则其值将为 None
)
id
- 项目的 ID。title
- 项目的标题。authors
- 作者(以及文章的潜在合著者)的NameIdPair
列表。summary
- 可选。项目的摘要。如果是文章/博客/提示,则包含描述(实际上可以为空,因此此成员的值可以为None
,但这不会经常发生),否则(问题/答案/消息)包含前几行。content_type
- 类似于doc_type
(见下文),但它对问题和答案也返回Article
,因为 CodeProject 的系统将文章用于问题和答案[^]doc_type
- 带有“文档类型”名称和 ID 的NameIdPair
,可以是以下任意一种doc_type
的名称/ID 对注意:这些文档类型不区分大小写。 ID 名称 1 文章 2 技术博客 3 提示/技巧 4 问题 5 答案 6 论坛消息 13 reference categories
- 项目类别的NameIdPair
列表。tags
- 项目标签的NameIdPair
列表。license
- 可选。项目许可证的NameIdPair
。created_date
- 显示项目创建的 UTC 日期modified_date
- 显示项目上次修改的 UTC 日期。如果尚未修改,则显示创建日期。thread_editor
- 可选。如果适用,包含讨论线程最新编辑者的NameIdPair
。thread_modification_date
- 可选。如果适用,包含讨论线程的最新修改日期。rating
- 项目的评分votes
- 项目的投票数量popularity
- 项目的受欢迎程度。来自 热门文章页面: “受欢迎程度衡量文章被阅读后产生的兴趣。它计算为评分 x Log10(# 投票)
,其中评分为过滤后的评分。有关详细信息,请参阅 FAQ。”website_link
- 指向网站上项目的链接api_link
- 尚未实现[^]parent_id
- 项目父项的 ID。如果它没有父项,则为0
。thread_id
- 讨论线程的 ID。如果项目不是论坛消息,则此值为0
。indent_level
- 论坛消息的“缩进级别”:线程为0
,回复为1
,回复的回复为2
等。如果项目不是论坛消息,则此值为0
。
这是 ItemSummary
类的构造函数
def __init__(self, data):
self.id = data['id']
self.title = data['title']
self.authors = NameIdPair.data_to_pairs(data['authors'])
self.summary = data['summary']
self.content_type = data['contentType']
self.doc_type = NameIdPair(data['docType'])
self.categories = NameIdPair.data_to_pairs(data['categories'])
self.tags = NameIdPair.data_to_pairs(data['tags'])
if 'license' in data and data['license'] is not None and \
data['license']['name'] is not None:
self.license = NameIdPair(data['license'])
else:
self.license = None
self.created_date = data['createdDate']
self.modified_date = data['modifiedDate']
if 'threadEditor' in data and data['threadEditor'] is not None\
and data['threadEditor']['name'] is not None:
self.thread_editor = NameIdPair(data['threadEditor'])
else:
self.thread_editor = None
if 'threadModifiedDate' in data:
self.thread_modified_date = data['threadModifiedDate']
else:
self.thread_modified_date = None
self.rating = data['rating']
self.votes = data['votes']
self.popularity = data['popularity']
self.website_link = data['websiteLink']
self.api_link = data['apiLink']
self.parent_id = data['parentId']
self.thread_id = data['threadId']
self.indent_level = data['indentLevel']
构造函数用 API 的响应填充所有成员,并用 None
填充以下字段(如果它们不存在):license
、thread_editor
和 thread_modified_date
。请注意,其中一些字段可能存在于 API 响应中,但仍然表示它不存在。例如,如果 API 响应中的 license
是 {"id": 0, "name": null}
,则表示没有许可证。我的 API 封装器仍然将其标记为 None
。
该类还有一个静态 data_to_items
方法,用于将 API 响应中的 ItemSummary
列表转换为 ItemSummary
类的实例。此方法在 ItemSummaryListViewModel
构造函数中使用。它的工作方式与 NameIdPair
的 data_to_pairs
方法相同。
@staticmethod
def data_to_items(data):
return [ItemSummary(x) for x in data]
PaginationInfo
PaginationInfo
由 ItemSummaryListViewModel
使用,它表示分页。它具有以下成员
page
- 当前页page_size
- 每页大小total_pages
- 总页数total_items
- 总项目数
构造函数没有什么特别之处,它只是用解析后的 API 响应中的数据填充实例的成员。
class PaginationInfo():
def __init__(self, data):
self.page = data['page']
self.page_size = data['pageSize']
self.total_pages = data['totalPages']
self.total_items = data['totalItems']
MyNotificationsViewModel
MyNotificationsViewModel
由 My
API 的 notifications
页面返回。它有一个成员:notifications
,它是一个 NotificationViewModel
列表。
from .notificationviewmodel import NotificationViewModel
class MyNotificationsViewModel():
def __init__(self, data):
self.notifications = NotificationViewModel.data_to_notifications(
data['notifications']
)
NotificationViewModel.data_to_notifications
是一个方法,它将解析后的 JSON 字典列表转换为 NotificationViewModel
列表。
NotificationViewModel
此视图模型存储有关通知的信息,例如当有人回答您的问题、回复您的消息、评论您的文章时,您会收到通知......
NotificationViewModel
具有以下成员
id
- 通知的 ID。object_type_name,
- 我不完全确定这些代表什么。[^]object_id
,topic
subject
- 通知的标题。notification_date
- 您收到通知的日期。unread
- 指示通知是否未读的布尔值。content
- 通知的内容。link
- 通知链接。
如上一节所述,NotificationViewModel
类有一个静态 data_to_notifications
方法。它的工作方式与 NameIdPair
中的 data_to_pairs
相同,只是类不同。
class NotificationViewModel():
def __init__(self, data):
self.id = data['id']
self.object_type_name = data['objectTypeName']
self.object_id = data['objectId']
self.subject = data['subject']
self.topic = data['topic']
self.notification_date = data['notificationDate']
self.unread = data['unRead']
self.content = data['content']
self.link = data['link']
@staticmethod
def data_to_notifications(data):
return [NotificationViewModel(x) for x in data]
MyProfileViewModel
此视图模型包含您个人资料的所有信息,包括公共和私有信息。请注意,您不必担心有人会使用 API 窃取您的私人数据:我的 API 仅在您登录后才可访问,并且您只能使用它来获取您的信息,而不是其他人的。如果您在网站上未填写可选值,API 将为此项返回一个空字符串。该视图模型具有以下成员
id
- 包含用户 ID。注意:这与codeproject_member_id
不同(见下文)。id
是 用于跨站点识别的值[^]。user_name
- 您的用户名。用于https://codeproject.org.cn/Members/User-Name
的那个display_name
- 您的显示名称,显示在您的问答帖子底部、消息旁边等。avatar
- 您的个人资料图片 URL。email
- 您的电子邮件地址。html_emails
- 指示您是否接受 HTML 电子邮件的布尔值。country
- 您的国家/地区。home_page
- 您的主页 URL。codeproject_member_id
- 您在 CodeProject 上的会员 ID,您可以在https://codeproject.org.cn/script/Membership/View.aspx?mid=<member ID>
中找到member_profile_page_url
- 您的个人资料页面 URL。twitter_name
- 您在 Twitter 上的名称。google_plus_profile
- 您的 Google+ 个人资料 ID。linkedin_profile
- 您的 LinkedIn 个人资料 ID。biography
- 您提供的简历。company
- 您工作的公司。job_title
- 您的职位。
MyProfileViewModel
的构造函数与任何其他视图模型的构造函数相同:它用解析后的 JSON 字典中的数据填充上述成员。
class MyProfileViewModel():
def __init__(self, data):
self.id = data['id']
self.user_name = data['userName']
self.display_name = data['displayName']
self.avatar = data['avatar']
self.email = data['email']
self.html_emails = data['htmlEmails']
self.country = data['country']
self.home_page = data['homePage']
self.codeproject_member_id = data['codeProjectMemberId']
self.member_profile_page_url = data['memberProfilePageUrl']
self.twitter_name = data['twitterName']
self.google_plus_profile = data['googlePlusProfile']
self.linkedin_profile = data['linkedInProfile']
self.biography = data['biography']
self.company = data['company']
self.job_title = data['jobTitle']
MyReputationViewModel
此视图模型包含您在 CodeProject 上的声誉信息。它具有以下成员
total_points
- 您的总声誉积分。reputation_types
- 一个ReputationTypeViewModel
列表(见下一节),用于将总声誉积分划分为不同的部分。graph_url
- 您的声誉图表的 URL。
from .reputationtypeviewmodel import ReputationTypeViewModel
class MyReputationViewModel():
def __init__(self, data):
self.total_points = data['totalPoints']
self.reputation_types = ReputationTypeViewModel.data_to_types(
data['reputationTypes']
)
self.graph_url = data['graphUrl']
ReputationTypeViewModel
此视图模型包含一种声誉类型的声誉积分数量。它在 MyReputationViewModel
中使用。它具有以下成员
name
- 声誉类型的名称。points
- 您为此声誉类型拥有的积分数量。designation
- 与名称和级别一起出现的声誉级别的名称(见下文)。例如,如果您是白金作者,则为Legend
。level
- 您在该声誉类型中的称号,例如silver
。
ReputationTypeViewModel
还有一个静态 data_to_types
方法,用于将解析后的 JSON 字典列表转换为 ReputationTypeViewModel
列表。它的工作方式与 NameIdPair
的 data_to_pairs
完全相同。
class ReputationTypeViewModel():
def __init__(self, data):
self.name = data['name']
self.points = data['points']
self.level = data['level']
self.designation = data['designation']
@staticmethod
def data_to_types(data):
return [ReputationTypeViewModel(x) for x in data]
类方法从 API 获取数据并将其存储在视图模型中
我们现在已经了解了所有视图模型。但它们有什么用呢?在 CPApiWrapper
类中,有许多方法使用 api_request
方法并将其响应存储在视图模型中。这些方法的目的是让您的生活更轻松,因为您不必自己使用 api_request
方法。
所有这些方法都是实例方法。您可以在创建封装器实例并获取访问令牌后调用它们。示例
from CPApiWrapper.cpapiwrapper import CPApiWrapper
# ^ assumes that the Python package is in a subdirectory called "CPApiWrapper"
wrapper = CPApiWrapper()
wrapper.get_access_token("Client ID here", "Client Secret here") # optionally email and password
latest_articles = wrapper.get_articles()
最新文章:get_articles
此方法使用 Articles API 获取最新文章列表。它返回一个 ItemSummaryListViewModel
并具有以下可选参数
tags
- 期望一个逗号分隔的标签列表。只有包含这些标签的文章才会返回。默认值为None
(即所有标签)。min_rating
- 返回文章的最低评分。默认值为3.0
。page
- 要显示的页面。默认值为1
。
def get_articles(self, tags=None, min_rating=3.0, page=1):
data = self.api_request("/Articles", {"tags": tags,
"minRating": min_rating,
"page": page})
return ItemSummaryListViewModel(data)
获取论坛消息:get_messages_from_forum
get_messages_from_forum
方法调用 /Forum/{forum ID}/{mode}
来获取论坛中的最新消息。该方法具有以下参数
forum_id
- 您要获取消息的论坛 ID。mode
- 论坛的显示模式,可以是消息或主题。请参阅下文了解更多信息。默认是主题。page
- 要显示的页面。默认值为1
。
mode
有两个有效值:"Messages"
和 "Threads"
。您可以将这些字符串字面量中的任何一个作为参数传递给 get_messages_from_forum
方法,但您也可以使用 ForumDisplayMode
类的成员。该类有两个成员:members
和 threads
。我建议使用该类的成员:如果 mode
的有效值发生变化,只需在一个地方进行更改。
def get_messages_from_forum(self, forum_id, mode=ForumDisplayMode.threads,
page=1):
data = self.api_request("/Forum/{0}/{1}".format(forum_id, mode),
{"page": page})
return ItemSummaryListViewModel(data)
获取线程中的消息:get_messages_from_thread
您还可以使用 get_messages_from_thread
方法获取特定线程中的论坛消息。它调用 /MessageThread/{id}/messages
来完成此操作,并将响应存储在 ItemSummaryViewModel
中。该方法具有以下参数
thread_id
- 消息线程的 IDpage
- 要显示的页面。默认值为1
。
def get_messages_from_thread(self, thread_id, page=1):
data = self.api_request(
"/MessageThread/{0}/messages".format(thread_id),
{"page": page}
)
return ItemSummaryListViewModel(data)
最新问题:get_questions
get_questions
方法获取最新活动/新/未回答的问题,并返回一个 ItemSummaryViewModel
。它接受以下参数
mode
- 指定您要最新问题、最新活动问题还是最新未回答问题。默认值为QuestionListMode.active
。include
- 问题必须包含的逗号分隔标签。默认值为None
(即所有标签)ignore
- 问题不得包含的逗号分隔标签。默认值为None
(即无忽略标签)page
- 要显示的页面。默认值为1
。
QuestionListMode
是一个类似于 ForumDisplayMode
的类。它有三个成员:active
、new
和 unanswered
。
def get_questions(self, mode=QuestionListMode.active, include=None,
ignore=None, page=1):
data = self.api_request("/Questions/{0}".format(mode),
{"include": include,
"ignore": ignore,
"page": page})
return ItemSummaryListViewModel(data)
我的 API
访问我的 API 的方法是最简单和最相似的方法。它们仅返回类型不同,其中一些带有 page
参数,另一些则没有。此参数始终是可选的,默认值为 1
。
get_my_answers
- 使用/my/answers
获取您的最新答案。返回一个ItemSummaryViewModel
。有一个可选的page
参数。get_my_articles
- 使用/my/articles
获取您的文章。返回一个ItemSummaryViewModel
。有一个可选的page
参数。get_my_blog_posts
- 使用/my/blogposts
获取您的博客文章。返回一个ItemSummaryViewModel
。有一个可选的page
参数。get_my_bookmarks
- 使用/my/bookmarks
获取您收藏的项目。返回一个ItemSummaryViewModel
。有一个可选的page
参数。get_my_messages
- 使用/my/messages
获取您的最新论坛消息。返回一个ItemSummaryViewModel
。有一个可选的page
参数。get_my_notifications
- 使用/my/notifications
获取您的未读通知。返回一个MyNotificationsViewModel
。get_my_profile
- 使用/my/profile
获取您的个人资料信息。返回一个MyProfileViewModel
。get_my_questions
- 使用/my/questions
获取您的最新问题。返回一个ItemSummaryViewModel
。有一个可选的page
参数。get_my_reputation
- 使用/my/reputation
获取您的声誉积分。返回一个MyReputationViewModel
。get_my_tips
- 使用/my/tips
获取您的提示。返回一个ItemSummaryViewModel
。有一个可选的page
参数。
def get_my_answers(self, page=1):
data = self.api_request("/my/answers", {"page": page})
return ItemSummaryListViewModel(data)
def get_my_articles(self, page=1):
data = self.api_request("/my/articles", {"page": page})
return ItemSummaryListViewModel(data)
def get_my_blog_posts(self, page=1):
data = self.api_request("/my/blogposts", {"page": page})
return ItemSummaryListViewModel(data)
def get_my_bookmarks(self, page=1):
data = self.api_request("/my/bookmarks", {"page": page})
return ItemSummaryListViewModel(data)
def get_my_messages(self, page=1):
data = self.api_request("/my/messages", {"page": page})
return ItemSummaryListViewModel(data)
def get_my_notifications(self):
data = self.api_request("/my/notifications")
return MyNotificationsViewModel(data)
def get_my_profile(self):
data = self.api_request("/my/profile")
return MyProfileViewModel(data)
def get_my_questions(self, page=1):
data = self.api_request("/my/questions", {"page": page})
return ItemSummaryListViewModel(data)
def get_my_reputation(self):
data = self.api_request("/my/reputation")
return MyReputationViewModel(data)
def get_my_tips(self, page=1):
data = self.api_request("/my/tips", {"page": page})
return ItemSummaryListViewModel(data)
测试用例
所有方法和视图模型都已创建。为了确保没有错误,我们需要有测试用例。由于 API 响应是动态的,我们不能有检查静态输出的测试用例。相反,测试检查我们是否从 API 获取到完整数据(即视图模型是否正确填充),在不应抛出异常的地方没有异常,以及在预期时是否抛出异常。
测试用例使用两个封装器:一个未经电子邮件/密码认证,一个经过认证。两者都应该适用于非“我的 API”,只有第二个应该适用于“我的 API”。
测试用例可以在 test_apiwrapper.py
中找到。该文件开头有几个方法,用于检查视图模型是否完全填充(可选成员除外,如果有的话)。它们都遵循相同的模式:首先检查视图模型实例是否不为 None
,然后检查所有非视图模型属性是否为 None
,并且所有视图模型属性是否完整。这意味着这些断言方法可能会调用其他断言方法。
断言方法的一个例子,assert_nameidpair_is_complete
def assert_nameidpair_is_complete(value):
assert value is not None
assert value.name is not None
assert value.id is not None
它首先断言 value
(传递的 NameIdPair
实例)不为 None
,然后对所有成员执行相同的操作。如果任何断言失败,将抛出 AssertionError
。
我不会将其他断言方法复制到文章中:它们都看起来相同,唯一的区别是有些视图模型具有可选成员。
文章封装器方法测试:test_get_articles
在断言方法之后,您可以找到用于验证封装器特定部分是否正常工作的方法。这些方法都具有相同的方法签名:它们都接受一个参数,该参数应该是 CPApiWrapper
的实例。
test_get_articles
方法首先验证 CPApiWrapper
的 get_articles
方法在没有传递任何参数(即使用所有默认参数值)时是否返回正确的数据。它首先检查返回的 ItemSummaryViewModel
是否完整,然后检查返回的页面是否确实是第 1 页,并且所有文章的评分都在 3.0
或以上。
articles = w.get_articles()
assert_itemsummarylistviewmodel_is_complete(articles)
assert articles.pagination.page == 1
for article in articles.items:
assert article.rating >= 3.0
test_get_articles
的下一个测试将标签列表、最低评分和页码传递给 get_articles
。收到响应后,它会检查返回的 ItemSummaryViewModel
是否完整,以及所有文章是否符合指定的标准。
articles = w.get_articles("C#,C++", min_rating=4.0, page=3)
assert_itemsummarylistviewmodel_is_complete(articles)
assert articles.pagination.page == 3
for article in articles.items:
assert article.rating >= 4.0
tags_upper = [tag.name.upper() for tag in article.tags]
assert "C#" in tags_upper or "C++" in tags_upper
test_get_articles
的最后一个测试验证了 min_rating
也适用于浮点数。4.5
作为 min_rating
参数传递给 get-articles
,其他参数是默认值。
articles = w.get_articles(min_rating=4.5)
assert_itemsummarylistviewmodel_is_complete(articles)
assert articles.pagination.page == 1
for article in articles.items:
assert article.rating >= 4.5
问题封装器测试:test_get_questions
下一个测试方法是 test_get_questions
,用于测试 CPApiWrapper
的 get_questions
方法。它首先遍历所有 QuestionListMode
,并使用当前模式和所有标签发送页面 1 问题的 API 请求。它断言返回的 ItemSummaryViewModel
完整且确实获取了页面 1。
for m in [QuestionListMode.unanswered, QuestionListMode.active,
QuestionListMode.new]:
questions = w.get_questions(m)
assert_itemsummarylistviewmodel_is_complete(questions)
assert questions.pagination.page == 1
之后,该方法测试 get_questions
的 include
参数是否有效。在收到 ItemSummaryViewModel
后,它检查所有问题是否具有所需标签之一。测试用例使用 HTML
和 CSS
作为包含标签。
questions = w.get_questions(include="HTML,CSS")
assert_itemsummarylistviewmodel_is_complete(questions)
for question in questions.items:
tags_upper = [tag.name.upper() for tag in question.tags]
assert "HTML" in tags_upper or "CSS" in tags_upper
检查包含标签后,接着检查被忽略的标签。这部分代码与包含标签的检查非常相似,只是我们现在检查问题是否未被标记为任何被忽略的标签。
questions = w.get_questions(ignore="C#,SQL")
assert_itemsummarylistviewmodel_is_complete(questions)
for question in questions.items:
tags_upper = [tag.name.upper() for tag in question.tags]
assert "C#" not in tags_upper and "SQL" not in tags_upper
该方法的最后一部分是检查 page
参数是否正常工作。在 get_questions
返回 ItemSummaryViewModel
后,该方法断言它已完成,并且当前页面是 2
,与传递给 get_questions
的值相同。
questions = w.get_questions(page=2)
assert_itemsummarylistviewmodel_is_complete(questions)
assert questions.pagination.page == 2
测试论坛消息封装器方法:test_get_forum_messages
测试 get_forum_messages
的方法非常简单:它遍历所有 ForumDisplayMode
,从 ID 为 1159 的论坛(Lounge[^])加载当前模式下的最新消息,并断言返回的 ItemSummaryViewModel
是完整的。
def test_get_forum_messages(w):
for m in [ForumDisplayMode.messages, ForumDisplayMode.threads]:
messages = w.get_messages_from_forum(1159, m)
assert_itemsummarylistviewmodel_is_complete(messages)
测试消息线程封装器方法:test_get_thread_messages
此方法测试 CPApiWrapper
的 get_messages_from_thread
方法。它以 消息 5058566[^] 作为线程调用该方法,并断言返回的 ItemSummaryViewModel
是完整的。
def test_get_thread_messages(w):
messages = w.get_messages_from_thread(5058566)
assert_itemsummarylistviewmodel_is_complete(messages)
测试我的 API 封装器方法:test_my
“我的 API”的所有不同封装器方法都以相同的方式进行测试:调用方法,并断言返回的视图模型是完整的。对于带有可选页面参数的方法,我们进行两次调用:一次使用 page = 1(默认值),一次使用 page = 2。
然而,对于大多数视图模型,在断言它们完整之前,我们必须首先检查 <视图模型实例>.items
的长度是否大于零,如果不是,则不要尝试断言视图模型完整,因为我们将收到断言错误。我们之前的测试方法不需要这样做,但在这里必须这样做,因为无法确定您是否在网站上发布了文章、答案、问题等,如果您没有,.items
将为空,这在这里是完全有效的情况。
def test_my(w):
answers = w.get_my_answers()
if len(answers.items) > 0:
assert_itemsummarylistviewmodel_is_complete(answers)
assert answers.pagination.page == 1
answers = w.get_my_answers(page=2)
if len(answers.items) > 0:
assert_itemsummarylistviewmodel_is_complete(answers)
assert answers.pagination.page == 2
articles = w.get_my_articles()
if len(articles.items) > 0:
assert_itemsummarylistviewmodel_is_complete(articles)
assert articles.pagination.page == 1
articles = w.get_my_articles(page=2)
if len(articles.items) > 0:
assert_itemsummarylistviewmodel_is_complete(articles)
assert articles.pagination.page == 2
blog = w.get_my_blog_posts()
if len(blog.items) > 0:
assert_itemsummarylistviewmodel_is_complete(blog)
assert blog.pagination.page == 1
blog = w.get_my_blog_posts(page=2)
if len(blog.items) > 0:
assert_itemsummarylistviewmodel_is_complete(blog)
assert blog.pagination.page == 2
bookmarks = w.get_my_bookmarks()
if len(bookmarks.items) > 0:
assert_itemsummarylistviewmodel_is_complete(bookmarks)
assert bookmarks.pagination.page == 1
bookmarks = w.get_my_bookmarks(page=2)
if len(bookmarks.items) > 0:
assert_itemsummarylistviewmodel_is_complete(bookmarks)
assert bookmarks.pagination.page == 2
messages = w.get_my_messages()
if len(messages.items) > 0:
assert_itemsummarylistviewmodel_is_complete(messages)
assert messages.pagination.page == 1
messages = w.get_my_messages(page=2)
if len(messages.items) > 0:
assert_itemsummarylistviewmodel_is_complete(messages)
assert messages.pagination.page == 2
notifications = w.get_my_notifications()
assert_mynotificationsviewmodel_is_complete(notifications)
profile = w.get_my_profile()
assert_myprofileviewmodel_is_complete(profile)
questions = w.get_my_questions()
if len(questions.items) > 0:
assert_itemsummarylistviewmodel_is_complete(questions)
assert questions.pagination.page == 1
questions = w.get_my_questions(page=2)
if len(questions.items) > 0:
assert_itemsummarylistviewmodel_is_complete(questions)
assert questions.pagination.page == 2
rep = w.get_my_reputation()
assert_myrepviewmodel_is_complete(rep)
tips = w.get_my_tips()
if len(tips.items) > 0:
assert_itemsummarylistviewmodel_is_complete(tips)
assert tips.pagination.page == 1
tips = w.get_my_tips(page=2)
if len(tips.items) > 0:
assert_itemsummarylistviewmodel_is_complete(tips)
assert tips.pagination.page == 2
执行测试用例
我们现在已经看完了所有的测试方法,但是也应该有执行这些测试的代码,这些代码可以在 test_my
方法之后找到。首先要做的,是向用户询问客户端 ID、客户端密钥、CodeProject 登录电子邮件和密码。这有两种选择:将所有内容作为命令行参数提供,或者通过标准输入提供所有内容。参数可以在 sys.argv
变量中找到(需要 import sys
)。如果此列表的长度为 4
,则使用命令行参数,否则,向用户请求输入。
if len(sys.argv) == 4:
client_id = sys.argv[0]
client_secret = sys.argv[1]
email = sys.argv[2]
password = sys.argv[3]
else:
try:
input_ = raw_input # Python 2
except NameError:
input_ = input # Python 3
client_id = input_("Client ID: ")
client_secret = input_("Client Secret: ")
email = input_("Email: ")
password = getpass("Password: ")
封装器适用于 Python 2 和 Python 3,因此测试用例也应适用于这两个版本。这里我们看到了一个区别:在 Python 2 中,raw_input
应该用于提示客户端 ID/密钥和电子邮件(Python 2 的 input
基本上是 eval(raw_input())
),但在 Python 3 中,raw_input
不再存在,而被称为 input
。我们将正确的输入方法存储为 input_
。然后我们为客户端 ID、客户端密钥和登录电子邮件调用此函数。但是对于密码,我们使用 getpass
(需要 from getpass import getpass
),它会隐藏用户输入,适用于密码。
然后,封装器被初始化。初始化后,我们断言两个封装器的 access_token_data
成员都为 None
。然后我们尝试使用其中一个封装器获取最新文章。这必须失败并引发 AccessTokenNotFetchedException
,因为封装器尚未获取访问令牌。
wrapper1 = CPApiWrapper()
wrapper2 = CPApiWrapper()
assert wrapper1.access_token_data is None
assert wrapper2.access_token_data is None
try:
wrapper1.get_articles()
assert False # If the API key is not fetched, it should raise an exception
except AccessTokenNotFetchedException:
pass # Correct behavior
之后,获取访问令牌。wrapper1
不传递电子邮件和密码,而 wrapper2
传递。
print("Fetching access token")
wrapper1.get_access_token(client_id, client_secret)
wrapper2.get_access_token(client_id, client_secret, email, password)
然后是时候运行两个封装器的 test_...
方法了。我们使用 for
循环遍历它们,并将当前的一个作为参数传递给测试方法。请注意,test_my
在 for 循环外部调用:它只应针对 wrapper 2 进行测试。
i = 0
for wr in [wrapper1, wrapper2]:
i += 1
print("Testing for wrapper {0}".format(i))
print("Testing /Articles")
test_get_articles(wr)
print("Testing /Questions")
test_get_questions(wr)
print("Testing /Forum")
test_get_forum_messages(wr)
print("Testing /MessageThread")
test_get_thread_messages(wr)
print("Testing /my")
test_my(wrapper2)
我们还需要测试,如果我们尝试使用 wrapper 1 访问 My API,封装器是否会抛出异常:我们没有在那里提供电子邮件和密码。
print("Testing that /my throws an error without email/password")
try:
wrapper1.api_request("/my/articles")
assert False
except ApiRequestFailedException:
pass
在测试文件末尾,如果所有测试都成功通过,则有一个打印语句,打印 Test passed!
print("Tests passed!")
现在我们已经了解了 CodeProject API 封装器的工作原理。我希望这篇文章和封装器能对您有所帮助。感谢阅读!