Add Terms of Service

pull/419/head
halcy 2025-08-17 23:43:12 +03:00
rodzic 1e4293ae3e
commit 02d38084ea
11 zmienionych plików z 481 dodań i 28 usunięć

Wyświetl plik

@ -2,8 +2,9 @@ A note on versioning: This librarys major version will grow with the APIs
version number. Breaking changes will be indicated by a change in the minor
(or major) version number, and will generally be avoided.
v2.1.0 (IN PROGRESS)
--------------------
v2.1.0
------
* Bumped support level to 4.4.3
* Fixed to_json breaking on python 3.14 (Thanks @limburgher for the report)
* Replaced pytest-vcr (deprecated) with pytest-recording (Thanks @CyberTailor)
* Improved timeline documentation (Thanks @adamse)
@ -27,6 +28,7 @@ v2.1.0 (IN PROGRESS)
* Added `date_of_birth` parameter to `create_account`.
* Added `account_endorse` and `account_unendorse` methods (replacing "pin" methods)
* Added `tag_feature` and `tag_unfeature` methods (replacing previous featured tag api)
* Added `instance_terms_of_service` method.
v2.0.1
------

Wyświetl plik

@ -7,7 +7,7 @@ Refer to mastodon changelog and API docs for details when implementing, add or m
* [x] Fix all the issues
* [x] New endpoints for endorsements, replacing "pin" api, which is now deprecated: accounts_endorsements(id), account_endorse(id), account_unendorse(id)
* [x] New endpoints for featured tags: tag_feature(name), tag_unfeature(name)
* [ ] New endpoint: instance_terms, with or without date (format?)
* [x] New endpoint: instance_terms, with or without date (format?)
* [x] Some oauth stuff (userinfo? capability discovery? see issue for that)
* [x] status_delete now has a media delete param
* [x] push_subscribe now has a "standard" parameter to switch between two versions. may also need to update crypto impls?

Wyświetl plik

@ -398,6 +398,9 @@ Return types
.. autoclass:: mastodon.return_types.OAuthUserInfo
:members:
.. autoclass:: mastodon.return_types.TermsOfService
:members:
Deprecated types
================
.. autoclass:: mastodon.return_types.Filter

Wyświetl plik

@ -19,6 +19,7 @@ current instance as well as data from the instance-wide profile directory.
.. automethod:: Mastodon.instance_nodeinfo
.. automethod:: Mastodon.instance_rules
.. automethod:: Mastodon.instance_extended_description
.. automethod:: Mastodon.instance_terms_of_service
Profile directory
~~~~~~~~~~~~~~~~~

Wyświetl plik

@ -4,10 +4,12 @@ from mastodon.utility import api_version
from mastodon.compat import urlparse
from mastodon.internals import Mastodon as Internals
from mastodon.return_types import Instance, InstanceV2, NonPaginatableList, Activity, Nodeinfo, AttribAccessDict, Rule, Announcement, CustomEmoji, Account, IdType, ExtendedDescription, DomainBlock, SupportedLocale
from mastodon.return_types import Instance, InstanceV2, NonPaginatableList, Activity, Nodeinfo, AttribAccessDict, Rule, Announcement, CustomEmoji, Account, IdType, ExtendedDescription, DomainBlock, SupportedLocale, TermsOfService
from typing import Union, Optional, Dict, List
import datetime
class Mastodon(Internals):
###
# Reading data: Instances
@ -127,6 +129,22 @@ class Mastodon(Internals):
"""
return self.__api_request('GET', '/api/v1/instance/rules')
@api_version("4.4.0", "4.4.0")
def instance_terms_of_service(self, date: Optional[datetime.date] = None) -> TermsOfService:
"""
Retrieve the instance's terms of service.
If `date` is specified, it will return the terms of service that were put in effect on that date.
NB: This is not (currently?) a range lookup, you can only get the terms of service for a specific, exact date.
"""
if date is not None and not isinstance(date, datetime.date):
raise MastodonIllegalArgumentError("Date parameter should be a datetime.date object")
if date is not None:
date = date.strftime("%Y-%m-%d")
params = self.__generate_params(locals())
return self.__api_request('GET', '/api/v1/instance/terms_of_service', params)
###
# Reading data: Directory
###

Wyświetl plik

@ -7178,6 +7178,55 @@ class OAuthUserInfo(AttribAccessDict):
_version = "4.4.0"
class TermsOfService(AttribAccessDict):
"""
The terms of service for the instance.
Example:
.. code-block:: python
# Returns a TermsOfService object
mastodon.instance_terms_of_service()
See also (Mastodon API documentation): https://docs.joinmastodon.org/methods/instance/#terms_of_service
"""
effective_date: "datetime"
"""
The date when the terms of service became effective.
Version history:
* 4.4.0: added
"""
effective: "bool"
"""
Whether the terms of service are currently in effect.
Version history:
* 4.4.0: added
"""
content: "str"
"""
The contents of the terms of service.
Should contain (as text): HTML
Version history:
* 4.4.0: added
"""
succeeded_by: "Optional[datetime]"
"""
If there are newer terms of service, their effective date. (optional)
Version history:
* 4.4.0: added
"""
_version = "4.4.0"
ENTITY_NAME_MAP = {
"Account": Account,
"AccountField": AccountField,
@ -7295,6 +7344,7 @@ ENTITY_NAME_MAP = {
"SupportedLocale": SupportedLocale,
"OAuthServerInfo": OAuthServerInfo,
"OAuthUserInfo": OAuthUserInfo,
"TermsOfService": TermsOfService,
}
__all__ = [
"Account",
@ -7413,5 +7463,6 @@ __all__ = [
"SupportedLocale",
"OAuthServerInfo",
"OAuthUserInfo",
"TermsOfService",
]

Wyświetl plik

@ -209,7 +209,7 @@ if sys.version_info < (3, 9):
FilterKeyword, FilterStatus, IdentityProof, StatusSource, Suggestion, Translation, \
AccountCreationError, AccountCreationErrorDetails, AccountCreationErrorDetailsField, NotificationPolicy, NotificationPolicySummary, RelationshipSeveranceEvent, \
GroupedNotificationsResults, PartialAccountWithAvatar, NotificationGroup, AccountWarning, UnreadNotificationsCount, Appeal, \
NotificationRequest, SupportedLocale, OAuthServerInfo, OAuthUserInfo
NotificationRequest, SupportedLocale, OAuthServerInfo, OAuthUserInfo, TermsOfService
if isinstance(t, ForwardRef):
try:
t = t._evaluate(globals(), locals(), frozenset())

Wyświetl plik

@ -80,17 +80,8 @@
"# Here you can test things manually during development\n",
"# results = {}\n",
"import pickle as pkl\n",
"mastodon_soc.instance_v1()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(json_data)\n",
"Entity.from_json(json_data)"
"#mastodon_soc.oauth_authorization_server_info()\n",
"#mastodon_ico_admin.instance_terms_of_service(datetime(2025, 8, 17))"
]
},
{
@ -101,6 +92,15 @@
"### Entity verification"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"results = {}"
]
},
{
"cell_type": "code",
"execution_count": null,
@ -108,8 +108,8 @@
"outputs": [],
"source": [
"entities = json.load(open(\"return_types.json\", \"r\"))\n",
"# update_only = \"RuleTranslation\"\n",
"update_only = None\n",
"update_only = \"TermsOfService\"\n",
"# update_only = None\n",
"\n",
"if update_only is None:\n",
" results = {}\n",
@ -207,15 +207,6 @@
" print(entity_name + \": field\", field, \"documented but missing from all retrieved entities\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"mastodon_soc.featured_tags()[0]"
]
},
{
"attachments": {},
"cell_type": "markdown",

Wyświetl plik

@ -10552,5 +10552,58 @@
"is_nullable": false
}
}
},
{
"name": "Instance Terms of Service",
"python_name": "TermsOfService",
"func_call": "mastodon.instance_terms_of_service()",
"func_call_real": null,
"func_call_additional": "mastodon.instance_terms_of_service(datetime(2025, 8, 17))",
"func_alternate_acc": true,
"manual_update": false,
"masto_doc_link": "https://docs.joinmastodon.org/methods/instance/#terms_of_service",
"description": "The terms of service for the instance.",
"fields": {
"effective_date": {
"description": "The date when the terms of service became effective.",
"enum": null,
"version_history": [["4.4.0", "added"]],
"field_type": "datetime",
"field_subtype": null,
"field_structuretype": null,
"is_optional": false,
"is_nullable": false
},
"effective": {
"description": "Whether the terms of service are currently in effect.",
"enum": null,
"version_history": [["4.4.0", "added"]],
"field_type": "bool",
"field_subtype": null,
"field_structuretype": null,
"is_optional": false,
"is_nullable": false
},
"content": {
"description": "The contents of the terms of service.",
"enum": null,
"version_history": [["4.4.0", "added"]],
"field_type": "str",
"field_subtype": null,
"field_structuretype": "HTML",
"is_optional": false,
"is_nullable": false
},
"succeeded_by": {
"description": "If there are newer terms of service, their effective date.",
"enum": null,
"version_history": [["4.4.0", "added"]],
"field_type": "datetime",
"field_subtype": null,
"field_structuretype": null,
"is_optional": true,
"is_nullable": false
}
}
}
]

Wyświetl plik

@ -0,0 +1,312 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate, br
Authorization:
- DUMMY
Connection:
- keep-alive
User-Agent:
- mastodonpy
method: GET
uri: https://icosahedron.website/api/v1/instance/terms_of_service
response:
body:
string: '{"effective_date": "2025-08-17", "effective": false, "content": "<h1>What
information do we collect?</h1>\n\n<p>We collect information from you when
you register on our site and gather data when you participate in the forum
by reading, writing, and evaluating the content shared here.</p>\n\n<p>When
registering on our site, you may be asked to enter your name and e-mail address.
You may, however, visit our site without registering. Your e-mail address
will be verified by an email containing a unique link. If that link is visited,
we know that you control the e-mail address.</p>\n\n<p>When registered and
posting, we record the IP address that the post originated from. We also may
retain server logs which include the IP address of every request to our server
and diagnostic information for errors.\nWhat do we use your information for?</p>\n\n<p>Any
of the information we collect from you may be used in one of the following
ways:</p>\n\n<ul>\n<li>To function as a social media website - we display
and share the posts that you make, associated with a pseudonym of your choosing.
We do not require you to provide your legal name to use this website.</li>\n<li>To
personalize your experience - your information helps us to better respond
to your individual needs.</li>\n<li>To improve service - your information
helps us to more effectively respond to your support needs and to fix operational
problems.</li>\n<li>To send periodic emails - The email address you provide
may be used to send you information, notifications that you request about
changes to topics or in response to your user name, respond to inquiries,
and/or other requests or questions. You can opt out of automatic e-mails by
indicating that you do not with to receive notification e-mails in the website
settings.</li>\n<li>To prevent abuse - your information helps us to moderate
the content on this website and to prevent use of the website that contravenes
our rules.</li>\n</ul>\n\n<h1>How do we protect your information?</h1>\n\n<p>We
implement a variety of security measures to maintain the safety of your personal
information when you enter, submit, or access your personal information. Communication
is encrypted using modern cryptographic standards (Using SSL with a certificate
signed by LetsEncrypt). Full access to data is restricted to people who unavoidably
require such access to perform maintenance operations, such as the administrator
(located in Estonia).</p>\n\n<p>All data we collect is stored on a dedicated
server in Germany, operated by Hetzner Online GmbH. A daily backup copy of
all data excluding log files is stored on a computer operated by the web site
administrator on private premises in Estonia. A continuous streaming backup
is additonally stored on a second server operated by Hetzner Online GmbH,
located in Finland.</p>\n\n<h1>What is your data retention policy?</h1>\n\n<p>We
will make a good faith effort to:</p>\n\n<ul>\n<li>Retain server logs containing
the IP address of all requests to this server no more than 30 days.</li>\n<li>Retain
the IP addresses associated with registered users and their posts no more
than 5 years.</li>\n</ul>\n\n<h1>Data portability</h1>\n\n<p>You can export
most of the data we collect about you using site features. You can contact
us via e-mail to get exports of your data beyond that.</p>\n\n<h1>Do we use
cookies?</h1>\n\n<p>Yes. Cookies are small files that a site or its service
provider transfers to your computer&#39;s hard drive through your Web browser
(if you allow). These cookies enable the site to recognize your browser and,
if you have a registered account, associate it with your registered account.
We use cookies to understand and save your preferences for future visits and
compile aggregate data about site traffic and site interaction so that we
can offer better site experiences and tools in the future.</p>\n\n<h1>Do we
disclose any information to outside parties?</h1>\n\n<p>We do not sell, trade,
or otherwise transfer to outside parties your personally identifiable information.
This does not include trusted third parties who assist us in operating our
site, so long as those parties agree to keep this information confidential.
We may also release your information when we believe release is appropriate
to comply with the law, enforce our site policies, or protect ours or others
rights, property, or safety. Non-personally identifiable visitor and aggregate
information may be provided to other parties to help improve the website.</p>\n\n<p>We
do transfer some of your data, such as posts you make and the pseudonym with
which you have registered on this website, to other servers in the ActivityPub
and OStatus networks. We also store and display such data that we receive
from other servers on the network. We do this because it is neccesary to do
it for this website to provide the services of a distributed social network.
You can opt out of this by only allowing people from your local server to
follow you. If you use web notifications, we share your data (in an encrypted
form) with your web notification provider. You can opt out of this by not
using web notifications.</p>\n\n<p>This website is hosted on a server located
in a Hetzner datacenter in Germany. While we do not explicitly share any information
with them and they do not regularly process any of your data, they may as
part of maintenance operations or to comply with the law gain access to that
data. You can review their privacy policy on the Hetzner website.\nThird party
links</p>\n\n<p>Occasionally, we or our users may include or offer third party
products or services on our site. These third party sites have separate and
independent privacy policies. We therefore have no responsibility or liability
for the content and activities of these linked sites. Nonetheless, we seek
to protect the integrity of our site and welcome any feedback about these
sites.\nAge restrictions</p>\n\n<p>Our site and services are all directed
to people who are at least 16 years old. If you are under 16, you must get
parental consent to be allowed to use this website. If you are below the age
of 13, you may not use this website.</p>\n\n<h1>Online Privacy Policy Only</h1>\n\n<p>This
online privacy policy applies only to information collected through our site
and not to information collected offline.</p>\n\n<h1>Contact</h1>\n\n<p>All
inquiries and requests (for export, correction, deletion or other things)
regarding the use of your data on this website should be directed to lorenzd+icosa@gmail.com.</p>\n\n<h1>Your
Consent</h1>\n\n<p>By using our site, you consent to our web site privacy
policy and to us using your data in the ways described here.\nChanges to our
Privacy Policy</p>\n\n<p>If we decide to change our privacy policy, we will
post those changes on this page.</p>\n\n<p>This document is CC-BY-SA. It was
originally adapted from the Discourse privacy policy.</p>\n", "succeeded_by":
"2025-08-27"}'
headers:
Connection:
- keep-alive
Content-Type:
- application/json; charset=utf-8
Date:
- Sun, 17 Aug 2025 20:41:50 GMT
Transfer-Encoding:
- chunked
Vary:
- Accept-Encoding
- Accept, Origin
X-Cached:
- HIT
cache-control:
- max-age=300, public, stale-while-revalidate=30, stale-if-error=86400
content-length:
- '7695'
content-security-policy:
- default-src 'none'; frame-ancestors 'none'; form-action 'none'
etag:
- W/"35a393f5110a365869e8429ce7155fb6"
referrer-policy:
- same-origin
server:
- Mastodon
strict-transport-security:
- max-age=63072000; includeSubDomains
- max-age=31536000
x-content-type-options:
- nosniff
x-frame-options:
- DENY
x-ratelimit-limit:
- '300'
x-ratelimit-remaining:
- '298'
x-ratelimit-reset:
- '2025-08-17T20:40:00.693662Z'
x-request-id:
- a98c4cfe-c91f-472c-baec-ef6d2a120521
x-runtime:
- '0.013353'
x-xss-protection:
- '0'
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate, br
Authorization:
- DUMMY
Connection:
- keep-alive
User-Agent:
- mastodonpy
method: GET
uri: https://icosahedron.website/api/v1/instance/terms_of_service?date=2025-08-17
response:
body:
string: '{"effective_date": "2025-08-17", "effective": false, "content": "<h1>What
information do we collect?</h1>\n\n<p>We collect information from you when
you register on our site and gather data when you participate in the forum
by reading, writing, and evaluating the content shared here.</p>\n\n<p>When
registering on our site, you may be asked to enter your name and e-mail address.
You may, however, visit our site without registering. Your e-mail address
will be verified by an email containing a unique link. If that link is visited,
we know that you control the e-mail address.</p>\n\n<p>When registered and
posting, we record the IP address that the post originated from. We also may
retain server logs which include the IP address of every request to our server
and diagnostic information for errors.\nWhat do we use your information for?</p>\n\n<p>Any
of the information we collect from you may be used in one of the following
ways:</p>\n\n<ul>\n<li>To function as a social media website - we display
and share the posts that you make, associated with a pseudonym of your choosing.
We do not require you to provide your legal name to use this website.</li>\n<li>To
personalize your experience - your information helps us to better respond
to your individual needs.</li>\n<li>To improve service - your information
helps us to more effectively respond to your support needs and to fix operational
problems.</li>\n<li>To send periodic emails - The email address you provide
may be used to send you information, notifications that you request about
changes to topics or in response to your user name, respond to inquiries,
and/or other requests or questions. You can opt out of automatic e-mails by
indicating that you do not with to receive notification e-mails in the website
settings.</li>\n<li>To prevent abuse - your information helps us to moderate
the content on this website and to prevent use of the website that contravenes
our rules.</li>\n</ul>\n\n<h1>How do we protect your information?</h1>\n\n<p>We
implement a variety of security measures to maintain the safety of your personal
information when you enter, submit, or access your personal information. Communication
is encrypted using modern cryptographic standards (Using SSL with a certificate
signed by LetsEncrypt). Full access to data is restricted to people who unavoidably
require such access to perform maintenance operations, such as the administrator
(located in Estonia).</p>\n\n<p>All data we collect is stored on a dedicated
server in Germany, operated by Hetzner Online GmbH. A daily backup copy of
all data excluding log files is stored on a computer operated by the web site
administrator on private premises in Estonia. A continuous streaming backup
is additonally stored on a second server operated by Hetzner Online GmbH,
located in Finland.</p>\n\n<h1>What is your data retention policy?</h1>\n\n<p>We
will make a good faith effort to:</p>\n\n<ul>\n<li>Retain server logs containing
the IP address of all requests to this server no more than 30 days.</li>\n<li>Retain
the IP addresses associated with registered users and their posts no more
than 5 years.</li>\n</ul>\n\n<h1>Data portability</h1>\n\n<p>You can export
most of the data we collect about you using site features. You can contact
us via e-mail to get exports of your data beyond that.</p>\n\n<h1>Do we use
cookies?</h1>\n\n<p>Yes. Cookies are small files that a site or its service
provider transfers to your computer&#39;s hard drive through your Web browser
(if you allow). These cookies enable the site to recognize your browser and,
if you have a registered account, associate it with your registered account.
We use cookies to understand and save your preferences for future visits and
compile aggregate data about site traffic and site interaction so that we
can offer better site experiences and tools in the future.</p>\n\n<h1>Do we
disclose any information to outside parties?</h1>\n\n<p>We do not sell, trade,
or otherwise transfer to outside parties your personally identifiable information.
This does not include trusted third parties who assist us in operating our
site, so long as those parties agree to keep this information confidential.
We may also release your information when we believe release is appropriate
to comply with the law, enforce our site policies, or protect ours or others
rights, property, or safety. Non-personally identifiable visitor and aggregate
information may be provided to other parties to help improve the website.</p>\n\n<p>We
do transfer some of your data, such as posts you make and the pseudonym with
which you have registered on this website, to other servers in the ActivityPub
and OStatus networks. We also store and display such data that we receive
from other servers on the network. We do this because it is neccesary to do
it for this website to provide the services of a distributed social network.
You can opt out of this by only allowing people from your local server to
follow you. If you use web notifications, we share your data (in an encrypted
form) with your web notification provider. You can opt out of this by not
using web notifications.</p>\n\n<p>This website is hosted on a server located
in a Hetzner datacenter in Germany. While we do not explicitly share any information
with them and they do not regularly process any of your data, they may as
part of maintenance operations or to comply with the law gain access to that
data. You can review their privacy policy on the Hetzner website.\nThird party
links</p>\n\n<p>Occasionally, we or our users may include or offer third party
products or services on our site. These third party sites have separate and
independent privacy policies. We therefore have no responsibility or liability
for the content and activities of these linked sites. Nonetheless, we seek
to protect the integrity of our site and welcome any feedback about these
sites.\nAge restrictions</p>\n\n<p>Our site and services are all directed
to people who are at least 16 years old. If you are under 16, you must get
parental consent to be allowed to use this website. If you are below the age
of 13, you may not use this website.</p>\n\n<h1>Online Privacy Policy Only</h1>\n\n<p>This
online privacy policy applies only to information collected through our site
and not to information collected offline.</p>\n\n<h1>Contact</h1>\n\n<p>All
inquiries and requests (for export, correction, deletion or other things)
regarding the use of your data on this website should be directed to lorenzd+icosa@gmail.com.</p>\n\n<h1>Your
Consent</h1>\n\n<p>By using our site, you consent to our web site privacy
policy and to us using your data in the ways described here.\nChanges to our
Privacy Policy</p>\n\n<p>If we decide to change our privacy policy, we will
post those changes on this page.</p>\n\n<p>This document is CC-BY-SA. It was
originally adapted from the Discourse privacy policy.</p>\n", "succeeded_by":
"2025-08-27"}'
headers:
Connection:
- keep-alive
Content-Type:
- application/json; charset=utf-8
Date:
- Sun, 17 Aug 2025 20:41:51 GMT
Transfer-Encoding:
- chunked
Vary:
- Accept-Encoding
- Accept, Origin
X-Cached:
- HIT
cache-control:
- max-age=300, public, stale-while-revalidate=30, stale-if-error=86400
content-length:
- '7695'
content-security-policy:
- default-src 'none'; frame-ancestors 'none'; form-action 'none'
etag:
- W/"35a393f5110a365869e8429ce7155fb6"
referrer-policy:
- same-origin
server:
- Mastodon
strict-transport-security:
- max-age=63072000; includeSubDomains
- max-age=31536000
x-content-type-options:
- nosniff
x-frame-options:
- DENY
x-ratelimit-limit:
- '300'
x-ratelimit-remaining:
- '299'
x-ratelimit-reset:
- '2025-08-17T20:40:00.006257Z'
x-request-id:
- c3d993b2-7a1a-4a55-acd8-6daadbde0be0
x-runtime:
- '0.014082'
x-xss-protection:
- '0'
status:
code: 200
message: OK
version: 1

Wyświetl plik

@ -1818,3 +1818,25 @@ def test_entity_oauthuserinfo(mastodon_base, mastodon_admin):
if sys.version_info >= (3, 9):
assert real_issubclass(type(result), OAuthUserInfo), str(type(result)) + ' is not a subclass of OAuthUserInfo after to_json/from_json'
@pytest.mark.vcr(
filter_query_parameters=[('access_token', 'DUMMY'), ('client_id', 'DUMMY'), ('client_secret', 'DUMMY')],
filter_post_data_parameters=[('access_token', 'DUMMY'), ('client_id', 'DUMMY'), ('client_secret', 'DUMMY')],
filter_headers=[('Authorization', 'DUMMY')],
before_record_request=vcr_filter,
before_record_response=token_scrubber,
match_on=['method', 'uri'],
cassette_library_dir='tests/cassettes_entity_tests'
)
def test_entity_termsofservice(mastodon_base, mastodon_admin):
mastodon = mastodon_admin
result = mastodon.instance_terms_of_service()
assert real_issubclass(type(result), TermsOfService), str(type(result)) + ' is not a subclass of TermsOfService'
result = Entity.from_json(result.to_json())
if sys.version_info >= (3, 9):
assert real_issubclass(type(result), TermsOfService), str(type(result)) + ' is not a subclass of TermsOfService after to_json/from_json'
result = mastodon.instance_terms_of_service(datetime(2025, 8, 17))
assert real_issubclass(type(result), TermsOfService), str(type(result)) + ' is not a subclass of TermsOfService (additional function)'
result = Entity.from_json(result.to_json())
if sys.version_info >= (3, 9):
assert real_issubclass(type(result), TermsOfService), str(type(result)) + ' is not a subclass of TermsOfService after to_json/from_json (additional function)'