From 02d38084ea714dae42923217e1a409466db94175 Mon Sep 17 00:00:00 2001 From: halcy Date: Sun, 17 Aug 2025 23:43:12 +0300 Subject: [PATCH] Add Terms of Service --- CHANGELOG.rst | 6 +- TODO.md | 2 +- docs/02_return_values.rst | 3 + docs/08_instances.rst | 3 +- mastodon/instance.py | 20 +- mastodon/return_types.py | 51 +++ mastodon/types_base.py | 2 +- srcgen/GenerateReturnTypes.ipynb | 35 +- srcgen/return_types.json | 53 +++ .../test_entity_termsofservice.yaml | 312 ++++++++++++++++++ tests/test_entities.py | 22 ++ 11 files changed, 481 insertions(+), 28 deletions(-) create mode 100644 tests/cassettes_entity_tests/test_entity_termsofservice.yaml diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a33be98..25b615a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 ------ diff --git a/TODO.md b/TODO.md index 5762c78..5a95fec 100644 --- a/TODO.md +++ b/TODO.md @@ -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? diff --git a/docs/02_return_values.rst b/docs/02_return_values.rst index d4d2bb4..8b6e6b4 100644 --- a/docs/02_return_values.rst +++ b/docs/02_return_values.rst @@ -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 diff --git a/docs/08_instances.rst b/docs/08_instances.rst index e6111a9..1b4c786 100644 --- a/docs/08_instances.rst +++ b/docs/08_instances.rst @@ -19,7 +19,8 @@ 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 ~~~~~~~~~~~~~~~~~ .. automethod:: Mastodon.directory diff --git a/mastodon/instance.py b/mastodon/instance.py index a0315d3..3d5dbbd 100644 --- a/mastodon/instance.py +++ b/mastodon/instance.py @@ -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 ### diff --git a/mastodon/return_types.py b/mastodon/return_types.py index 419f0c7..177fb93 100644 --- a/mastodon/return_types.py +++ b/mastodon/return_types.py @@ -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", ] diff --git a/mastodon/types_base.py b/mastodon/types_base.py index 4b42bd7..bfaec9b 100644 --- a/mastodon/types_base.py +++ b/mastodon/types_base.py @@ -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()) diff --git a/srcgen/GenerateReturnTypes.ipynb b/srcgen/GenerateReturnTypes.ipynb index 536263a..59c6aac 100644 --- a/srcgen/GenerateReturnTypes.ipynb +++ b/srcgen/GenerateReturnTypes.ipynb @@ -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", diff --git a/srcgen/return_types.json b/srcgen/return_types.json index f0a0245..cc24257 100644 --- a/srcgen/return_types.json +++ b/srcgen/return_types.json @@ -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 + } + } } ] diff --git a/tests/cassettes_entity_tests/test_entity_termsofservice.yaml b/tests/cassettes_entity_tests/test_entity_termsofservice.yaml new file mode 100644 index 0000000..59afc5f --- /dev/null +++ b/tests/cassettes_entity_tests/test_entity_termsofservice.yaml @@ -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": "

What + information do we collect?

\n\n

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.

\n\n

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.

\n\n

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?

\n\n

Any + of the information we collect from you may be used in one of the following + ways:

\n\n\n\n

How do we protect your information?

\n\n

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).

\n\n

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.

\n\n

What is your data retention policy?

\n\n

We + will make a good faith effort to:

\n\n\n\n

Data portability

\n\n

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.

\n\n

Do we use + cookies?

\n\n

Yes. Cookies are small files that a site or its service + provider transfers to your computer'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.

\n\n

Do we + disclose any information to outside parties?

\n\n

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.

\n\n

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.

\n\n

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

\n\n

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

\n\n

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.

\n\n

Online Privacy Policy Only

\n\n

This + online privacy policy applies only to information collected through our site + and not to information collected offline.

\n\n

Contact

\n\n

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.

\n\n

Your + Consent

\n\n

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

\n\n

If we decide to change our privacy policy, we will + post those changes on this page.

\n\n

This document is CC-BY-SA. It was + originally adapted from the Discourse privacy policy.

\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": "

What + information do we collect?

\n\n

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.

\n\n

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.

\n\n

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?

\n\n

Any + of the information we collect from you may be used in one of the following + ways:

\n\n\n\n

How do we protect your information?

\n\n

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).

\n\n

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.

\n\n

What is your data retention policy?

\n\n

We + will make a good faith effort to:

\n\n\n\n

Data portability

\n\n

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.

\n\n

Do we use + cookies?

\n\n

Yes. Cookies are small files that a site or its service + provider transfers to your computer'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.

\n\n

Do we + disclose any information to outside parties?

\n\n

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.

\n\n

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.

\n\n

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

\n\n

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

\n\n

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.

\n\n

Online Privacy Policy Only

\n\n

This + online privacy policy applies only to information collected through our site + and not to information collected offline.

\n\n

Contact

\n\n

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.

\n\n

Your + Consent

\n\n

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

\n\n

If we decide to change our privacy policy, we will + post those changes on this page.

\n\n

This document is CC-BY-SA. It was + originally adapted from the Discourse privacy policy.

\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 diff --git a/tests/test_entities.py b/tests/test_entities.py index 1dfc2d8..2956919 100644 --- a/tests/test_entities.py +++ b/tests/test_entities.py @@ -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)' +