##// END OF EJS Templates
slack: fixed celery serialization breaking slack_data dataclass
super-admin -
r5169:2045434b default
parent child Browse files
Show More
@@ -1,185 +1,189 b''
1 # Copyright (C) 2012-2023 RhodeCode GmbH
1 # Copyright (C) 2012-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19
19
20
20
21 import time
21 import time
22 import logging
22 import logging
23
23
24 import deform # noqa
24 import deform # noqa
25 import deform.widget
25 import deform.widget
26 import colander
26 import colander
27
27
28 from rhodecode import events
28 from rhodecode import events
29 from rhodecode.model.validation_schema.widgets import CheckboxChoiceWidgetDesc
29 from rhodecode.model.validation_schema.widgets import CheckboxChoiceWidgetDesc
30 from rhodecode.translation import _
30 from rhodecode.translation import _
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
32 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
33 from rhodecode.lib.colander_utils import strip_whitespace
33 from rhodecode.lib.colander_utils import strip_whitespace
34 from rhodecode.integrations.types.base import (
34 from rhodecode.integrations.types.base import (
35 IntegrationTypeBase,
35 IntegrationTypeBase,
36 requests_retry_call,
36 requests_retry_call,
37 )
37 )
38
38
39 from rhodecode.integrations.types.handlers.slack import SlackDataHandler, SlackData
39 from rhodecode.integrations.types.handlers.slack import SlackDataHandler, SlackData
40
40
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 class SlackSettingsSchema(colander.Schema):
45 class SlackSettingsSchema(colander.Schema):
46 service = colander.SchemaNode(
46 service = colander.SchemaNode(
47 colander.String(),
47 colander.String(),
48 title=_('Slack service URL'),
48 title=_('Slack service URL'),
49 description=h.literal(_(
49 description=h.literal(_(
50 'This can be setup at the '
50 'This can be setup at the '
51 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
51 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
52 'slack app manager</a>')),
52 'slack app manager</a>')),
53 default='',
53 default='',
54 preparer=strip_whitespace,
54 preparer=strip_whitespace,
55 validator=colander.url,
55 validator=colander.url,
56 widget=deform.widget.TextInputWidget(
56 widget=deform.widget.TextInputWidget(
57 placeholder='https://hooks.slack.com/services/...',
57 placeholder='https://hooks.slack.com/services/...',
58 ),
58 ),
59 )
59 )
60 username = colander.SchemaNode(
60 username = colander.SchemaNode(
61 colander.String(),
61 colander.String(),
62 title=_('Username'),
62 title=_('Username'),
63 description=_('Username to show notifications coming from.'),
63 description=_('Username to show notifications coming from.'),
64 missing='Rhodecode',
64 missing='Rhodecode',
65 preparer=strip_whitespace,
65 preparer=strip_whitespace,
66 widget=deform.widget.TextInputWidget(
66 widget=deform.widget.TextInputWidget(
67 placeholder='Rhodecode'
67 placeholder='Rhodecode'
68 ),
68 ),
69 )
69 )
70 channel = colander.SchemaNode(
70 channel = colander.SchemaNode(
71 colander.String(),
71 colander.String(),
72 title=_('Channel'),
72 title=_('Channel'),
73 description=_('Channel to send notifications to.'),
73 description=_('Channel to send notifications to.'),
74 missing='',
74 missing='',
75 preparer=strip_whitespace,
75 preparer=strip_whitespace,
76 widget=deform.widget.TextInputWidget(
76 widget=deform.widget.TextInputWidget(
77 placeholder='#general'
77 placeholder='#general'
78 ),
78 ),
79 )
79 )
80 icon_emoji = colander.SchemaNode(
80 icon_emoji = colander.SchemaNode(
81 colander.String(),
81 colander.String(),
82 title=_('Emoji'),
82 title=_('Emoji'),
83 description=_('Emoji to use eg. :studio_microphone:'),
83 description=_('Emoji to use eg. :studio_microphone:'),
84 missing='',
84 missing='',
85 preparer=strip_whitespace,
85 preparer=strip_whitespace,
86 widget=deform.widget.TextInputWidget(
86 widget=deform.widget.TextInputWidget(
87 placeholder=':studio_microphone:'
87 placeholder=':studio_microphone:'
88 ),
88 ),
89 )
89 )
90
90
91
91
92 class SlackIntegrationType(IntegrationTypeBase):
92 class SlackIntegrationType(IntegrationTypeBase):
93 key = 'slack'
93 key = 'slack'
94 display_name = _('Slack')
94 display_name = _('Slack')
95 description = _('Send events such as repo pushes and pull requests to '
95 description = _('Send events such as repo pushes and pull requests to '
96 'your slack channel.')
96 'your slack channel.')
97
97
98 @classmethod
98 @classmethod
99 def icon(cls):
99 def icon(cls):
100 return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M165.963541,15.8384262 C162.07318,3.86308197 149.212328,-2.69009836 137.239082,1.20236066 C125.263738,5.09272131 118.710557,17.9535738 122.603016,29.9268197 L181.550164,211.292328 C185.597902,222.478689 197.682361,228.765377 209.282098,225.426885 C221.381246,221.943607 228.756984,209.093246 224.896,197.21023 C224.749115,196.756984 165.963541,15.8384262 165.963541,15.8384262" fill="#DFA22F"></path><path d="M74.6260984,45.515541 C70.7336393,33.5422951 57.8727869,26.9891148 45.899541,30.8794754 C33.9241967,34.7698361 27.3710164,47.6306885 31.2634754,59.6060328 L90.210623,240.971541 C94.2583607,252.157902 106.34282,258.44459 117.942557,255.104 C130.041705,251.62282 137.417443,238.772459 133.556459,226.887344 C133.409574,226.436197 74.6260984,45.515541 74.6260984,45.515541" fill="#3CB187"></path><path d="M240.161574,166.045377 C252.136918,162.155016 258.688,149.294164 254.797639,137.31882 C250.907279,125.345574 238.046426,118.792393 226.07318,122.682754 L44.7076721,181.632 C33.5213115,185.677639 27.234623,197.762098 30.5731148,209.361836 C34.0563934,221.460984 46.9067541,228.836721 58.7897705,224.975738 C59.2430164,224.828852 240.161574,166.045377 240.161574,166.045377" fill="#CE1E5B"></path><path d="M82.507541,217.270557 C94.312918,213.434754 109.528131,208.491016 125.855475,203.186361 C122.019672,191.380984 117.075934,176.163672 111.76918,159.83423 L68.4191475,173.924721 L82.507541,217.270557" fill="#392538"></path><path d="M173.847082,187.591344 C190.235279,182.267803 205.467279,177.31777 217.195016,173.507148 C213.359213,161.70177 208.413377,146.480262 203.106623,130.146623 L159.75659,144.237115 L173.847082,187.591344" fill="#BB242A"></path><path d="M210.484459,74.7058361 C222.457705,70.8154754 229.010885,57.954623 225.120525,45.9792787 C221.230164,34.0060328 208.369311,27.4528525 196.393967,31.3432131 L15.028459,90.292459 C3.84209836,94.3380984 -2.44459016,106.422557 0.896,118.022295 C4.37718033,130.121443 17.227541,137.49718 29.1126557,133.636197 C29.5638033,133.489311 210.484459,74.7058361 210.484459,74.7058361" fill="#72C5CD"></path><path d="M52.8220328,125.933115 C64.6274098,122.097311 79.8468197,117.151475 96.1762623,111.84682 C90.8527213,95.4565246 85.9026885,80.2245246 82.0920656,68.4946885 L38.731541,82.5872787 L52.8220328,125.933115" fill="#248C73"></path><path d="M144.159475,96.256 C160.551869,90.9303607 175.785967,85.9803279 187.515803,82.1676066 C182.190164,65.7752131 177.240131,50.5390164 173.42741,38.807082 L130.068984,52.8996721 L144.159475,96.256" fill="#62803A"></path></g></svg>'''
100 return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M165.963541,15.8384262 C162.07318,3.86308197 149.212328,-2.69009836 137.239082,1.20236066 C125.263738,5.09272131 118.710557,17.9535738 122.603016,29.9268197 L181.550164,211.292328 C185.597902,222.478689 197.682361,228.765377 209.282098,225.426885 C221.381246,221.943607 228.756984,209.093246 224.896,197.21023 C224.749115,196.756984 165.963541,15.8384262 165.963541,15.8384262" fill="#DFA22F"></path><path d="M74.6260984,45.515541 C70.7336393,33.5422951 57.8727869,26.9891148 45.899541,30.8794754 C33.9241967,34.7698361 27.3710164,47.6306885 31.2634754,59.6060328 L90.210623,240.971541 C94.2583607,252.157902 106.34282,258.44459 117.942557,255.104 C130.041705,251.62282 137.417443,238.772459 133.556459,226.887344 C133.409574,226.436197 74.6260984,45.515541 74.6260984,45.515541" fill="#3CB187"></path><path d="M240.161574,166.045377 C252.136918,162.155016 258.688,149.294164 254.797639,137.31882 C250.907279,125.345574 238.046426,118.792393 226.07318,122.682754 L44.7076721,181.632 C33.5213115,185.677639 27.234623,197.762098 30.5731148,209.361836 C34.0563934,221.460984 46.9067541,228.836721 58.7897705,224.975738 C59.2430164,224.828852 240.161574,166.045377 240.161574,166.045377" fill="#CE1E5B"></path><path d="M82.507541,217.270557 C94.312918,213.434754 109.528131,208.491016 125.855475,203.186361 C122.019672,191.380984 117.075934,176.163672 111.76918,159.83423 L68.4191475,173.924721 L82.507541,217.270557" fill="#392538"></path><path d="M173.847082,187.591344 C190.235279,182.267803 205.467279,177.31777 217.195016,173.507148 C213.359213,161.70177 208.413377,146.480262 203.106623,130.146623 L159.75659,144.237115 L173.847082,187.591344" fill="#BB242A"></path><path d="M210.484459,74.7058361 C222.457705,70.8154754 229.010885,57.954623 225.120525,45.9792787 C221.230164,34.0060328 208.369311,27.4528525 196.393967,31.3432131 L15.028459,90.292459 C3.84209836,94.3380984 -2.44459016,106.422557 0.896,118.022295 C4.37718033,130.121443 17.227541,137.49718 29.1126557,133.636197 C29.5638033,133.489311 210.484459,74.7058361 210.484459,74.7058361" fill="#72C5CD"></path><path d="M52.8220328,125.933115 C64.6274098,122.097311 79.8468197,117.151475 96.1762623,111.84682 C90.8527213,95.4565246 85.9026885,80.2245246 82.0920656,68.4946885 L38.731541,82.5872787 L52.8220328,125.933115" fill="#248C73"></path><path d="M144.159475,96.256 C160.551869,90.9303607 175.785967,85.9803279 187.515803,82.1676066 C182.190164,65.7752131 177.240131,50.5390164 173.42741,38.807082 L130.068984,52.8996721 L144.159475,96.256" fill="#62803A"></path></g></svg>'''
101
101
102 valid_events = [
102 valid_events = [
103 events.PullRequestCloseEvent,
103 events.PullRequestCloseEvent,
104 events.PullRequestMergeEvent,
104 events.PullRequestMergeEvent,
105 events.PullRequestUpdateEvent,
105 events.PullRequestUpdateEvent,
106 events.PullRequestCommentEvent,
106 events.PullRequestCommentEvent,
107 events.PullRequestReviewEvent,
107 events.PullRequestReviewEvent,
108 events.PullRequestCreateEvent,
108 events.PullRequestCreateEvent,
109 events.RepoPushEvent,
109 events.RepoPushEvent,
110 events.RepoCreateEvent,
110 events.RepoCreateEvent,
111 ]
111 ]
112
112
113 def settings_schema(self):
113 def settings_schema(self):
114 schema = SlackSettingsSchema()
114 schema = SlackSettingsSchema()
115 schema.add(colander.SchemaNode(
115 schema.add(colander.SchemaNode(
116 colander.Set(),
116 colander.Set(),
117 widget=CheckboxChoiceWidgetDesc(
117 widget=CheckboxChoiceWidgetDesc(
118 values=sorted(
118 values=sorted(
119 [(e.name, e.display_name, e.description) for e in self.valid_events]
119 [(e.name, e.display_name, e.description) for e in self.valid_events]
120 ),
120 ),
121 ),
121 ),
122 description="List of events activated for this integration",
122 description="List of events activated for this integration",
123 name='events'
123 name='events'
124 ))
124 ))
125
125
126 return schema
126 return schema
127
127
128 def send_event(self, event):
128 def send_event(self, event):
129 log.debug('handling event %s with integration %s', event.name, self)
129 log.debug('handling event %s with integration %s', event.name, self)
130
130
131 if event.__class__ not in self.valid_events:
131 if event.__class__ not in self.valid_events:
132 log.debug('event %r not present in valid event list (%s)', event, self.valid_events)
132 log.debug('event %r not present in valid event list (%s)', event, self.valid_events)
133 return
133 return
134
134
135 if not self.event_enabled(event):
135 if not self.event_enabled(event):
136 return
136 return
137
137
138 data = event.as_dict()
138 data = event.as_dict()
139
139
140 handler = SlackDataHandler()
140 handler = SlackDataHandler()
141 slack_data = handler(event, data)
141 slack_data = handler(event, data)
142 # title, text, fields, overrides
142 # title, text, fields, overrides
143 run_task(post_text_to_slack, self.settings, slack_data)
143 run_task(post_text_to_slack, self.settings, slack_data)
144
144
145
145
146 @async_task(ignore_result=True, base=RequestContextTask)
146 @async_task(ignore_result=True, base=RequestContextTask)
147 def post_text_to_slack(settings, slack_data: SlackData):
147 def post_text_to_slack(settings, slack_data: SlackData):
148 # because JSON serialization, if we run async with celery, deserialize to SlackData
149 if isinstance(slack_data, dict):
150 slack_data = SlackData(**slack_data)
151
148 title = slack_data.title
152 title = slack_data.title
149 text = slack_data.text
153 text = slack_data.text
150 fields = slack_data.fields
154 fields = slack_data.fields
151 overrides = slack_data.overrides
155 overrides = slack_data.overrides
152
156
153 log.debug('sending %s (%s) to slack %s', title, text, settings['service'])
157 log.debug('sending %s (%s) to slack %s', title, text, settings['service'])
154
158
155 fields = fields or []
159 fields = fields or []
156 overrides = overrides or {}
160 overrides = overrides or {}
157
161
158 message_data = {
162 message_data = {
159 "fallback": text,
163 "fallback": text,
160 "color": "#427cc9",
164 "color": "#427cc9",
161 "pretext": title,
165 "pretext": title,
162 #"author_name": "Bobby Tables",
166 #"author_name": "Bobby Tables",
163 #"author_link": "http://flickr.com/bobby/",
167 #"author_link": "http://flickr.com/bobby/",
164 #"author_icon": "http://flickr.com/icons/bobby.jpg",
168 #"author_icon": "http://flickr.com/icons/bobby.jpg",
165 #"title": "Slack API Documentation",
169 #"title": "Slack API Documentation",
166 #"title_link": "https://api.slack.com/",
170 #"title_link": "https://api.slack.com/",
167 "text": text,
171 "text": text,
168 "fields": fields,
172 "fields": fields,
169 #"image_url": "http://my-website.com/path/to/image.jpg",
173 #"image_url": "http://my-website.com/path/to/image.jpg",
170 #"thumb_url": "http://example.com/path/to/thumb.png",
174 #"thumb_url": "http://example.com/path/to/thumb.png",
171 "footer": "RhodeCode",
175 "footer": "RhodeCode",
172 #"footer_icon": "",
176 #"footer_icon": "",
173 "ts": time.time(),
177 "ts": time.time(),
174 "mrkdwn_in": ["pretext", "text"]
178 "mrkdwn_in": ["pretext", "text"]
175 }
179 }
176 message_data.update(overrides)
180 message_data.update(overrides)
177 json_message = {
181 json_message = {
178 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
182 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
179 "channel": settings.get('channel', ''),
183 "channel": settings.get('channel', ''),
180 "username": settings.get('username', 'Rhodecode'),
184 "username": settings.get('username', 'Rhodecode'),
181 "attachments": [message_data]
185 "attachments": [message_data]
182 }
186 }
183 req_session = requests_retry_call()
187 req_session = requests_retry_call()
184 resp = req_session.post(settings['service'], json=json_message, timeout=60)
188 resp = req_session.post(settings['service'], json=json_message, timeout=60)
185 resp.raise_for_status() # raise exception on a failed request
189 resp.raise_for_status() # raise exception on a failed request
@@ -1,166 +1,178 b''
1
1 import dataclasses
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import io
20 import io
21 import datetime
21 import datetime
22 import decimal
22 import decimal
23 import textwrap
23 import textwrap
24
24
25 import pytest
25 import pytest
26
26
27 from rhodecode.lib import ext_json
27 from rhodecode.lib import ext_json
28 from rhodecode.translation import _, _pluralize
28 from rhodecode.translation import _, _pluralize
29
29
30
30
31 class Timezone(datetime.tzinfo):
31 class Timezone(datetime.tzinfo):
32 def __init__(self, hours):
32 def __init__(self, hours):
33 self.hours = hours
33 self.hours = hours
34
34
35 def utcoffset(self, unused_dt):
35 def utcoffset(self, unused_dt):
36 return datetime.timedelta(hours=self.hours)
36 return datetime.timedelta(hours=self.hours)
37
37
38
38
39 class SerializableObject(object):
39 class SerializableObject(object):
40 def __json__(self):
40 def __json__(self):
41 return 'foo'
41 return 'foo'
42
42
43
43
44 def test_dumps_set():
44 def test_dumps_set():
45 result = ext_json.json.dumps(set((1, 2, 3)))
45 result = ext_json.json.dumps(set((1, 2, 3)))
46 # We cannot infer what the order of result is going to be
46 # We cannot infer what the order of result is going to be
47 result = ext_json.json.loads(result)
47 result = ext_json.json.loads(result)
48 assert isinstance(result, list)
48 assert isinstance(result, list)
49 assert [1, 2, 3] == sorted(result)
49 assert [1, 2, 3] == sorted(result)
50
50
51
51
52 def test_dumps_decimal():
52 def test_dumps_decimal():
53 assert b'"1.5"' == ext_json.json.dumps(decimal.Decimal('1.5'))
53 assert b'"1.5"' == ext_json.json.dumps(decimal.Decimal('1.5'))
54
54
55
55
56 def test_dumps_complex():
56 def test_dumps_complex():
57 assert b"[0.0,1.0]" == ext_json.json.dumps(1j)
57 assert b"[0.0,1.0]" == ext_json.json.dumps(1j)
58 assert b"[1.0,0.0]" == ext_json.json.dumps(1 + 0j)
58 assert b"[1.0,0.0]" == ext_json.json.dumps(1 + 0j)
59 assert b"[1.1,1.2]" == ext_json.json.dumps(1.1 + 1.2j)
59 assert b"[1.1,1.2]" == ext_json.json.dumps(1.1 + 1.2j)
60
60
61
61
62 def test_dumps_object_with_json_method():
62 def test_dumps_object_with_json_method():
63 assert '"foo"' == ext_json.str_json(SerializableObject())
63 assert '"foo"' == ext_json.str_json(SerializableObject())
64
64
65
65
66 def test_dumps_object_with_json_attribute():
66 def test_dumps_object_with_json_attribute():
67
67
68 assert '"foo"' == ext_json.str_json(SerializableObject())
68 assert '"foo"' == ext_json.str_json(SerializableObject())
69
69
70
70
71 def test_dumps_time():
71 def test_dumps_time():
72 assert '"03:14:15.926535"' == ext_json.str_json(datetime.time(3, 14, 15, 926535))
72 assert '"03:14:15.926535"' == ext_json.str_json(datetime.time(3, 14, 15, 926535))
73
73
74
74
75 def test_dumps_time_no_microseconds():
75 def test_dumps_time_no_microseconds():
76 assert '"03:14:15"' == ext_json.str_json(datetime.time(3, 14, 15))
76 assert '"03:14:15"' == ext_json.str_json(datetime.time(3, 14, 15))
77
77
78
78
79 def test_dumps_time_with_timezone():
79 def test_dumps_time_with_timezone():
80 with pytest.raises(TypeError) as excinfo:
80 with pytest.raises(TypeError) as excinfo:
81 ext_json.json.dumps(datetime.time(3, 14, 15, 926535, Timezone(0)))
81 ext_json.json.dumps(datetime.time(3, 14, 15, 926535, Timezone(0)))
82
82
83 error_msg = str(excinfo.value)
83 error_msg = str(excinfo.value)
84
84
85 assert 'timezone library is not supported' in error_msg
85 assert 'timezone library is not supported' in error_msg
86 # only for simplejson
86 # only for simplejson
87 #assert 'Time-zone aware times are not JSON serializable' in error_msg
87 #assert 'Time-zone aware times are not JSON serializable' in error_msg
88
88
89
89
90 def test_dumps_date():
90 def test_dumps_date():
91 assert b'"1969-07-20"' == ext_json.json.dumps(datetime.date(1969, 7, 20))
91 assert b'"1969-07-20"' == ext_json.json.dumps(datetime.date(1969, 7, 20))
92
92
93
93
94 def test_dumps_datetime():
94 def test_dumps_datetime():
95 json_data = ext_json.json.dumps(datetime.datetime(1969, 7, 20, 3, 14, 15, 926535))
95 json_data = ext_json.json.dumps(datetime.datetime(1969, 7, 20, 3, 14, 15, 926535))
96 assert b'"1969-07-20T03:14:15.926535"' == json_data
96 assert b'"1969-07-20T03:14:15.926535"' == json_data
97
97
98
98
99 def test_dumps_datetime_no_microseconds():
99 def test_dumps_datetime_no_microseconds():
100 json_data = ext_json.json.dumps(datetime.datetime(1969, 7, 20, 3, 14, 15))
100 json_data = ext_json.json.dumps(datetime.datetime(1969, 7, 20, 3, 14, 15))
101 assert b'"1969-07-20T03:14:15"' == json_data
101 assert b'"1969-07-20T03:14:15"' == json_data
102
102
103
103
104 def test_dumps_datetime_with_utc_timezone():
104 def test_dumps_datetime_with_utc_timezone():
105 json_data = ext_json.json.dumps(
105 json_data = ext_json.json.dumps(
106 datetime.datetime(1969, 7, 20, 3, 14, 15, 926535, Timezone(0)))
106 datetime.datetime(1969, 7, 20, 3, 14, 15, 926535, Timezone(0)))
107 assert b'"1969-07-20T03:14:15.926535+00:00"' == json_data
107 assert b'"1969-07-20T03:14:15.926535+00:00"' == json_data
108
108
109
109
110 def test_dumps_datetime_with_plus1_timezone():
110 def test_dumps_datetime_with_plus1_timezone():
111 json_data = ext_json.json.dumps(
111 json_data = ext_json.json.dumps(
112 datetime.datetime(1969, 7, 20, 3, 14, 15, 926535, Timezone(1)))
112 datetime.datetime(1969, 7, 20, 3, 14, 15, 926535, Timezone(1)))
113 assert b'"1969-07-20T03:14:15.926535+01:00"' == json_data
113 assert b'"1969-07-20T03:14:15.926535+01:00"' == json_data
114
114
115
115
116 def test_dumps_unserializable_class():
116 def test_dumps_unserializable_class():
117 unserializable_obj = object()
117 unserializable_obj = object()
118 with pytest.raises(TypeError) as excinfo:
118 with pytest.raises(TypeError) as excinfo:
119 ext_json.json.dumps(unserializable_obj)
119 ext_json.json.dumps(unserializable_obj)
120
120
121 assert 'object' in str(excinfo.value)
121 assert 'object' in str(excinfo.value)
122 assert 'is not JSON serializable' in str(excinfo.value)
122 assert 'is not JSON serializable' in str(excinfo.value)
123
123
124
124
125 def test_dump_is_like_dumps():
125 def test_dump_is_like_dumps():
126 data = {
126 data = {
127 'decimal': decimal.Decimal('1.5'),
127 'decimal': decimal.Decimal('1.5'),
128 'set': set([1]), # Just one element to guarantee the order
128 'set': set([1]), # Just one element to guarantee the order
129 'complex': 1 - 1j,
129 'complex': 1 - 1j,
130 'datetime': datetime.datetime(1969, 7, 20, 3, 14, 15, 926535),
130 'datetime': datetime.datetime(1969, 7, 20, 3, 14, 15, 926535),
131 'time': datetime.time(3, 14, 15, 926535),
131 'time': datetime.time(3, 14, 15, 926535),
132 'date': datetime.date(1969, 7, 20),
132 'date': datetime.date(1969, 7, 20),
133 }
133 }
134 json_buffer = io.StringIO() # StringIO because dump uses simplejson not orjson
134 json_buffer = io.StringIO() # StringIO because dump uses simplejson not orjson
135 ext_json.json.dump(data, json_buffer)
135 ext_json.json.dump(data, json_buffer)
136
136
137 assert ext_json.sjson.dumps(data) == json_buffer.getvalue()
137 assert ext_json.sjson.dumps(data) == json_buffer.getvalue()
138
138
139
139
140 def test_formatted_json():
140 def test_formatted_json():
141 data = {
141 data = {
142 'b': {'2': 2, '1': 1},
142 'b': {'2': 2, '1': 1},
143 'a': {'3': 3, '4': 4},
143 'a': {'3': 3, '4': 4},
144 }
144 }
145
145
146 expected_data = textwrap.dedent('''
146 expected_data = textwrap.dedent('''
147 {
147 {
148 "a": {
148 "a": {
149 "3": 3,
149 "3": 3,
150 "4": 4
150 "4": 4
151 },
151 },
152 "b": {
152 "b": {
153 "1": 1,
153 "1": 1,
154 "2": 2
154 "2": 2
155 }
155 }
156 }''').strip()
156 }''').strip()
157
157
158 assert expected_data == ext_json.formatted_str_json(data)
158 assert expected_data == ext_json.formatted_str_json(data)
159
159
160
160
161 def test_lazy_translation_string(baseapp):
161 def test_lazy_translation_string(baseapp):
162 data = {'label': _('hello')}
162 data = {'label': _('hello')}
163 data2 = {'label2': _pluralize('singular', 'plural', 1)}
163 data2 = {'label2': _pluralize('singular', 'plural', 1)}
164
164
165 assert b'{"label":"hello"}' == ext_json.json.dumps(data)
165 assert b'{"label":"hello"}' == ext_json.json.dumps(data)
166 assert b'{"label2":"singular"}' == ext_json.json.dumps(data2)
166 assert b'{"label2":"singular"}' == ext_json.json.dumps(data2)
167
168
169 def test_serialize_dataclass():
170
171 @dataclasses.dataclass
172 class ExampleStruct:
173 field_str: str
174 field_int: int
175 struct = ExampleStruct(field_int=1, field_str='hello')
176 raw_struct = b'{"field_str":"hello","field_int":1}'
177 assert raw_struct == ext_json.json.dumps(struct)
178 assert struct == ExampleStruct(**ext_json.json.loads(raw_struct))
General Comments 0
You need to be logged in to leave comments. Login now